132 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
34d8a3c0e7 |
fix(chat): CH1 review fixes — sbb-idiom channel catch-all, command-output typing
Applies the Opus review findings on CH1 (
|
||
|
|
2cf94dbcd2 |
feat(audio): Ctrl+M instant mute + inn-chatter investigation closed as server content
Two items from listening-gate round 2. Mute: AcdreamToggleAudioMute (default Ctrl+M; bare M is selection) flips OpenAlAudioEngine.Muted, implemented as the AL LISTENER gain - unused since A2 moved all mixing to the CPU, so it is a free master switch that silences already-playing voices instantly and restores them exactly, without touching the retail mixing math, the -50 dB allocation cutoff, or any persisted volume setting. Rebindable like every other action; console line confirms each flip. Inn chatter: the user hears talk-and-laughter ambience in retail inns and not in acdream. Three installed-dat scans (pinned as conformance tests in EnvCellSoundEmitterInventoryTests) prove the mechanism is NOT client data: no interior static in the town landblock carries an ambient-slot sound table, no Setup among all 5,935 in the portal dat references one, and yet 23 sound tables carrying ONLY Ambient1..8 slots exist - pure soundscape banks with nothing client-side pointing at them. They are wire-bound: the server attaches one to an emitter object via CreateObject's sound-table field and fires the slots over 0xF750 - ACE implements exactly this (EmoteType.Sound heartbeat emotes -> GameMessageSound broadcast). Our 0xF750 receiver (slice A3) is live and now instrumented (ACDREAM_PROBE_SOUND_WIRE=1, via the new AudioDiagnostics owner per Code Structure Rule 5, with per-event drop reasons in AudioHookSink.PlayServerSound). A probed session against the local ACE received ZERO 0xF750 events across a town walkabout: the silence is server world-content (no emitters configured/firing), not a client drop. The first scan's assertion originally encoded the emitter-object hypothesis; the data refuted it, and the test now pins the negative so the conclusion cannot silently rot. Full Release suite green (the one failure during development was the hypothesis-pinning assertion, corrected to pin the finding). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e5ade796ac |
fix(audio): listening-gate round 1 — tunnel interior sound + ambience in houses (#355 gate)
Two user findings from the Campaign A listening session. 1. The portal tunnel's in-flight sound was silent while its enter/exit cues played. The tunnel's authored SoundTweakedHook drained into the world 3-D path at its synthetic owner's origin (0,0,0) — after A2 that dies twice: the listener is usually beyond the -50 dB no-allocate radius, and the world pool is suspended for the whole transit hold. The cues the user COULD hear were on the interface bus, which has neither problem, and retail's tunnel is gmSmartBoxUI — UI-owned — so that bus is also the faithful route. UiPresentationHookSink now wraps the shared router for the tunnel: sound-bearing hooks go from-centre through the interface bus (AudioHookSink.OnUiHook); every other hook kind still reaches the particle/lighting/translucency sinks unchanged. 2. Ambience cut dead inside houses; retail keeps the outdoor soundscape in sky-lit interiors. This is TS-66, now retired: the ambient listener source resolves the per-cell CEnvCell.seen_outside bit through the physics cache (the same #107 field AdjustPosition reads) and converts the envcell-local origin through the cell's WorldTransform into landblock coordinates before the 3x3 walk centres on it — an outdoor Position's origin is already landblock-local, an envcell's is cell-local, and skipping that conversion would centre the walk wrongly by up to a landblock. A not-yet-resident cell record resolves to silence for that rebuild rather than a wrong walk. Sealed dungeons stay silent, which is retail-correct. The user also reports interiors carrying their own local sound in retail (hearth-type emitters). Statics already register their sound tables and route animation hooks, so the expectation is that the seen_outside fix plus existing emitters covers it; re-listen decides, and anything still missing becomes a precise follow-up. Full Release suite: 11,740 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7c4dd1ade7 |
feat(audio): Campaign A slice A5 — retail's region ambient soundscape
acdream had no ambient system: StartAmbient minted a handle and played nothing. Retail's is a weighted-accumulation + timer-queue engine, not looping voices. On every objcell change (24 m) CellManager::ChangePosition rebuilds per-sound weights over the 3x3 landblock ring x 64 land cells each, decoding each cell's terrain word through the region file's terrain -> scene -> AmbientSTBDesc chain; playback is a min-heap of absolute deadlines drained from the frame tick, where each pop fires a one-shot and re-arms. A continuous bed (base_chance == 0) is non-positional, crossfaded by its share of the TOTAL weight, and re-fired every min_rate seconds — that rate is the author's intended loop period, and re-firing is how retail fakes a sustained bed with no looping voice, re-rolling the variant and the crossfade each time. An intermittent one keeps its authored volume, plays at a random accumulated compass bearing at min + (max-min)*t^2, and is dice-gated. Indoors is silent by design: CEnvCell's contributor is a folded ret and EnvCell carries no sound data. The Opus review caught four bugs before this landed, one fatal: - Cell offsets were built in ABSOLUTE world coordinates and differenced against the listener's STREAMED-frame position, so every one of 576 offsets came out ~32 km, every contribution was culled, and the whole feature was silent with nothing logged. Offsets are now landblock-local the way Position::get_offset builds them, and the streamed-frame position is carried separately for playback, where it belongs. - The cell's weight was added to the shared denominator once per DESCRIPTOR instead of once per CELL, dividing every bed's crossfade by the table's entry count — enough to push a typical authored volume under the 0.03 audibility floor. - The drain used where retail's UseTime is strictly below, so a descriptor authored with a zero rate re-armed at the same instant and spun the frame forever. - Arming only enqueued; retail's UpdatePlayQueue PLAYS and then re-arms, so a newly audible ambient was silent for a full period after the crossing that made it audible. Also: beds now go through retail's single 16-voice priority pool rather than acdream's UI pool (retail has one pool; parking beds in the UI pool let an A4 portal cue chop one mid-wave and discarded the authored priority), and CalcDir's in-block test is XY-only, since CalcWeight includes Z on purpose and CalcDir excludes it on purpose. Two behaviours are knowingly incomplete and registered rather than guessed at slice end: TS-66 (sky-lit interiors should keep the outdoor set) and TS-67 (contribution weight is computed in-plane). Retires TS-29. The frame-loop hook is a typed IAmbientFramePhase, not a callback — the first attempt used an Action<float> and the architecture guard ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks correctly rejected it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6eaa490bb3 |
feat(audio): Campaign A slice A4 — the interface sound bus
Retail's UI sound bank was absent, so three families of cue were silent: the portal enter/exit stingers, the AdminEnvirons dungeon atmosphere (chanting, drums, whispers, thunder — what players remember as dungeon 'music'), and every other interface slot. The bank's DID is not a literal anywhere in retail: GetUISoundTable @0x00563FB0 asks GetByEnum for enum slot 7, and DBCache::GetDIDFromEnum @0x00413940 resolves it through two EnumIDMap hops off the portal dat header's master map. UiSoundTableResolver walks that chain the way RetailCursorResolver already walks it for cursors. Against the shipped dats it resolves to 0x2000004B, and that table holds exactly the 32 UI_* slots (UI_EnterPortal 0x6A .. UI_Thunder6 0x8A) — content that confirms the walk independently of the decode. UiSoundTableResolutionTests pins the walk, the DID, and the content, and skips when dats are absent. Two corrections to the research along the way. The lane-5 note recorded GetByEnum's arguments transposed: the 0x22 it called a fileType is the CACHE type (CLOCache(cache, CSoundTable::Allocator, 0x22)) and the real second-hop key is 0x10000003; walking it the other way finds nothing. And its claim that the interface volume pref applies is wrong — GetAttenuation with ambient=0 multiplies by the EFFECT knob, so retail's interface_sound_volume stays the dead knob lane 1 byte-decoded it to be. EnvironSoundCueMap is an explicit 21-case table read straight out of Handle_Admin__Environs @0x0055DE20, not arithmetic: codes 0x65..0x72 sit 0x11 below their SoundType, but 0x73/0x74 have no case, so 0x75 lands on UI_Squeal (0x84) where an offset gives 0x86, and the switch ends at 0x7B with no 0x7C case. Verified case-by-case against the decomp rather than from the lane note, whose tail table was ambiguous. Cues are attached where retail plays them: the teleport-animation boundary for the portal pair, and the AdminEnvirons handler for the stingers. PlaySoundFromCenter's pan-0 / distance-0 shape is what PlayUiWave already implements after A2. Retires TS-54. Narrows AP-115 to its notice-presentation residual. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8bc458fb88 |
feat(audio): Campaign A slice A3 — the server sound channel (0xF750)
acdream never parsed retail's Sound event, so every server-driven cue was silent: melee hits and wounds, wield/unwield, pickup/drop, lockpicking, lifestone bind, spell resist, trap triggers, item mana depletion. SoundEvent parses the 16-byte message (guid, SoundType, f32 volume) whose layout three oracles agree on: retail CM_Physics::DispatchSB_SoundEvent @0x006AC760 reading buf+4/+8/+0xC, ACE's GameMessageSound at declared length 16, and holtburger's PlaySoundData. Playback reuses EntityEffectController's existing per-guid queue rather than adding a second one, because retail routes sounds through the SAME CObjectMaint blob queue as F754/F755: an event for a guid the client does not know yet is parked and drained by HandleCreateObject, so a creature that spawns and immediately grunts still grunts. Dropping it — the obvious alternative — would silently lose the cue. Sound joins Direct and Typed as a third PendingEffect kind so one readiness edge releases the whole mixed stream in order. AudioHookSink.PlayServerSound reproduces two decoded asymmetries with the animation-hook path: the sound plays at the WIRE volume and the SoundTable entry's volume is ignored (the hook path does the opposite), while the entry's probability still gates it and its priority still drives eviction. An object with no SoundTable plays nothing, matching CPhysicsObj::play_sound @0x0050F460's early return. The no-window host parses and discards, exactly as it does for F754/F755 — sound is presentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02b735ba4a |
fix(vendor): evidence-based pass — max-first stack ceiling; the local player resolves never-animated MoveTo targets
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Both chains pinned by the live [vendor-diag] run (vendor-diag.log) after three code-reading rounds each failed: The split bar: ACE serializes descStackSize=1 for EVERY browse row (live wire, log 343-348) — the R1-era "ACE never populates desc" claim is retracted with the line quoted. Retail's vendor sites read pwd._maxStackSize directly (four sites, incl. UpdateItemsList @0x004c1ea0 stamping min(remaining, _maxStackSize)); ResolveAuthoredStackSize flips to max-first for its vendor-only consumers. Taper ceiling 1000, scarab 100, seed 1 for exempt. Pricing still reads the desc (per-1 values on ACE). Walk-to-use: the local player's getObjectA seam was bound to TryGetPhysicsHost, which resolves only INSTALLED physics hosts — a never-animated vendor has none, so TargetManager.SetTarget got null, the MoveToObject armed with zero nodes, and UseTime never dispatched. The log's natural=False completions were the user's own movement keys (retail-correct input-edge cancels); attempt 4 worked because the greeting animation had installed a host. RuntimePhysicsState gains the retail CObjectMaint::GetObjectA seam (bound canonical resolver with installed-host fallback); the graphical host binds the SAME lazy-minimal-host resolver every remote already uses — whose own doc comment names this exact never-animated hazard. The reservation release was already correct (2b premise refuted with evidence); the production-wiring invariants are now pinned by four new tests including the pre-fix pathology as a permanent sabotage control. AP-169 rewritten a second time, honestly. The [vendor-diag] probe family (ACDREAM_DUMP_VENDOR) lands env-gated for future live triage. Clean-room complete solution: 11,536 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
92ea3977b6 |
feat(vendor): Slice 6b/6c — move-to-use, buy staging, selling; the vendor arc is functionally complete
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
C1 an out-of-range Use now approaches first via the existing client-predicted BeginApproach (Pickup's far-range shape mirrored; retail's ItemHolder::UseObject @0x00588A80 has no range check and the dispatch stays immediate). C2 Add-to-List stages into the Buying tab via VendorStagingList (RemoveProfileFromList's two shapes, pc:200497-200537), Buy All sends ONE batched 0x005F and flushes staging on send exactly as retail does (SendShopEvent -> Flush, pc:204075-204076 — not UseDone-gated), and X-close over a non-empty staging list shows retail's confirm string recovered verbatim from the binary data segment (0x007b5bd8) through the existing dialog factory. C3 the Selling tab's list is the sole drop target (retail's single IsAncestorOfMe gate, pc:204229-204246); VendorSellAcceptability ports InqAcceptability with all rejection strings recovered verbatim from the raw data segment; the sell side prices with BuyPrice (retail's inverted naming: what the vendor PAYS) and 0x0060 carries no trailing currency field, unlike Buy. C4 the status-bar reproduction test PASSES against the production toolbar mount — retail's toolbar shows count + name with the split bar and NO price parenthetical (that figure is the vendor row's own cost text); no code change, the live gate referees. C5 pack order verified correct, untouched. Register: AP-161 narrowed to its two pre-existing cosmetic gaps; AP-162 extended over Buy All; AP-164 (non-sellable bitfield unmodeled), AP-165 (DescStackSize for _maxStackSize in the removal test, bounded), AP-166 (purse text + pending-sell highlight cosmetic) filed. Clean-room complete solution: 11,482 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
3c9fc57adb |
fix(vendor): Slice 6 review corrections — ownership-checked retire, live slider display, drag-proof shop rows, hardened buy reservation
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
All nine findings from the buy-arc review, at root: F1 the materializer's retire pass re-checks ownership (guid->vendorId map; remove only while the live object's ContainerId still equals the recording vendor) — buying a player-sold UNIQUE no longer deletes the item you just purchased; the discriminating reparent-then-refresh test pins it. F2 the cost/name display subscribes to the live split state and shares ONE quantity computation with Buy (retail re-renders per slider tick: RecvNotice_StackSliderChanged 0x004C4500) — the sentence and the charge can no longer disagree. F3 shop rows never mint drag payloads (UiItemSlot.AllowDragSource gates both IsDragSource AND GetDragPayload — the second gate was caught by this pass's own test). F4 sendBuy reports whether anything was sent; a null-session buy cancels the reservation instead of leaking BusyCount forever. F5 the retire loop snapshots, isolates per-guid observer failures, and clears its tracking in finally and Dispose — teardown convergence can no longer wedge. F6 auto-select is retail's unconditional first-filtered-item shape (pc:201180-201184; the survival-check was our invention and the comment claiming otherwise is corrected). F7 non-stack buys clamp to quantity 1 locally (BuySingleItem pc:201669). F8 the Add button is hard-disabled until staging exists. F9 AP-161/162/163 rewritten to the post-fix reality. Clean-room complete solution: 11,378 passed / 4 skipped / 0 failed. The #350 render-ledger overflow observed this session is under separate investigation and is NOT addressed here. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
97cf873870 |
feat(vendor): Slice 6 buy arc — shop items are real objects, vendor selection is THE selection, and Buy works (0x005F)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Three ordered pieces in one landing (the shared controller/composition files carry all three; the internal order was 6.1 -> 6.2 -> 6.3): 6.1 VendorShopItemMaterializer diff-merges the shop list into the live ClientObjectTable on VendorState transitions (so client-local close and session teardown retire the entries too) and never claims a guid it did not add — ACE's UniqueItemsForSale can re-list a guid a player once held (AP-163 files the collision-skip; no retail counterpart traced). Right-click examine on shop items now routes through the ordinary appraisal path — the 5.4 F7c blocker dissolves with the table entries. 6.2 SelectionChangeSource.Vendor: row clicks, auto-select, and examine all flow through the canonical SelectionState; the status bar and the existing byte-faithful StackSplitQuantityState slider light up unmodified. VendorSplitPolicy is the single 0xDC41CB0 mask owner; the slider VALUE seeds to 1 for exempt items while maxSplitSize keeps the stack (the splitSize/maxSplitSize distinction, research §B.3). Selection clears at retail's actual site — VendorItemsUI::RemoveFromShop (pc:202848), not a CloseVendor-level clear that does not exist. 6.3 BuildBuy (0x005F): vendorGuid, count, (i32 amount, u32 guid) pairs, and the trailing alternateCurrencyId the REAL client sends (CM_Vendor::Event_Buy pc:689288) though ACE's reader ignores it. TryBuy rides the EXISTING J5.2 one-request-at-a-time reservation and completes on UseDone; the Buy button disables while a request is in flight. The reconciliation round-trip (money property update, inventory CreateObject, ApproachVendor refresh -> panel rebuild) is proven by a synthetic-inbound test against existing machinery — no new owner. Register: AP-161 narrowed (selection + examine residuals close; staging/Sell remain; double-click-to-buy confirmed ABSENT from retail with negative evidence cited — we match retail). AP-162 files the conscious no-client-side-affordability-precheck deferral. Clean-room complete solution: 11,368 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
c721830e71 |
feat(ui): Slice 5.4 — the authored vendor browse panel (LayoutDesc 0x21000012)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The vendor window is retail's own: LayoutDesc 0x21000012, root 0x100000B7, found by enumerating all 101 layouts for the one containing both known tab controls and clinched by the root's Type 0x10000017 — the literal UIElement::RegisterElementClass id for gmVendorUI (pc:202075). Discovery evidence and the D0 read live in the research doc's new §B.4. D0 corrected two assumptions: retail's category "tabs" are a UiMenu DROPDOWN fed by a hardcoded 18-row ordered category table (ported bit-for-bit against our ItemType enum; list always scoped to exactly one category, first-present wins, selection preserved across refresh per retail's clamp), and the layout authors THREE tabs — Items (browse, this slice), Buying and Selling (staged-transaction review, Slice 6) — decision 4's "browse/Buy tab" names the Items tab retail's mode-2 OpenTab opens. The non-default tabs render and switch pages but stay inert, fenced in comments. VendorUiController mounts Items: category dropdown, icon-cell item row with the retained scrollbar, per-unit retail pricing via VendorPricing.SellPrice (the vendor-stock path VendorProfile:: VendorSellPrice feeds), name/cost on selection. The panel is a pure projection of VendorState — opens on populate, closes on clear; the close button's VendorState.Close() is its only permitted mutation. Nothing on the wire. AP-110 narrowed (vendor leaves the absent-panels list); AP-161 files the precise Slice-6 remainder (Buying/Selling unwired, Buy/Add buttons, InqAcceptability). Twelve controller tests on a real-dat fixture. Clean-room complete solution: 11,323 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
9796d71522 |
feat(runtime): Slice 5.3 — RuntimeInventoryState owns the vendor browse session
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The sole VendorState joins the J4.2 inventory owners: populated by the new 0x0062 ApproachVendor route (parse via VendorApproach, wire-to- domain mapping at the routing seam, silent-drop on malformed like every sibling), borrowed by both graphical and headless hosts, and torn down through the EXISTING ExternalContainer reset stage — session reset, portal-out, and logout all funnel through the one mechanism. Close is client-local per retail (nothing on the wire): a range watcher rides the existing per-advanced-frame publishMovement callback, using the vendor's own authored UseRadius (ACE's 0.6 m fallback when absent). The dormant ItemInteractionController ActiveVendorId seam is finally wired as a live delegate — real id while open, 0 the moment the session clears. AP-160 filed in this same commit: the watcher measures plain 3D center distance rather than retail's cylinder-gap, because Runtime has no per-NPC collision radius/height source; bounded sub-meter, client- local UI only. Twelve Runtime tests: populate/field mapping, vendor replacement, range clear + within-range retention, all three generation teardowns, the ActiveVendorId seam, malformed-event drop. Clean-room complete solution: 11,302 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
52bdf4df71 |
fix(world): #344 — a mid-teleport world-frame disagreement defers the projection instead of crashing
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
During a portal transit the two world-frame owners legitimately rebase on different edges (Runtime at TeleportAdvanced, streaming only after old-window retirement), and a spawn projection landing in that window hit the #283 invariant as an unhandled render-path throw — the crash the user hit entering a dungeon. The guard's check is unchanged; only the disagreement RESPONSE is discriminated on the canonical transit authority (RuntimeWorldTransitState.IsTeleportActive, the same field the App layer already reads for portal-in-flight): in transit -> the materializer's existing "not yet" return, parking the projection on its established retry rides (OnLandblockLoaded's re-attempt loop, whose ordering guarantees agreement on retry because the recenter coordinator adopts the new origin BEFORE unblocking new landblock loads — verified at source; plus OnPosition recovery and OnAppearance). Outside transit -> still throws: genuine corruption stays loud. The implementer explicitly ruled out riding the Runtime placement pump, which would have acknowledged-and-discarded the completion receipt and silently dropped the entity forever. Sabotage: removing the discriminator reddened the pre-existing #283 throw tests as well as the new not-in-transit test — the sabotage defeats the original contract, not merely the new coverage. Four new tests cover defer, defer-then-agree-then-succeed (projected exactly once), throw-outside-transit, and the agreeing pass-through. Clean-room suite: 11,261 passed / 6 skipped / 0 failed. #346 filed for a sixth, distinct load-sensitive allocation flake observed during the runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55b07f6a62 |
refactor(physics): hoist the live-entity collision builder to Runtime (#330 groundwork)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
LiveEntityCollisionBuilder and LiveEntityDefaultPoseResolver move from AcDream.App.Physics to AcDream.Runtime.Physics with no behaviour change — diff-verified byte-identical shape math by both review lenses. The Build signature's App-record parameter is replaced by presentation-free primitives with identical guard semantics, INCLUDING the FinalPhysicsState read the contract had missed and the implementer surfaced rather than dropped. Visibility stays internal: Runtime's existing InternalsVisibleTo grants already cover every consumer, so the implementation's public widening is reverted per the architecture review's finding 11. The registration WIRING is deliberately WITHHELD. Both Opus lenses failed it, converging: a shadow registered at spawn freezes there (RuntimeRemotePhysicsUpdater is Runtime-homed but App-driven — nothing headless ticks it), so a walking NPC becomes a phantom obstacle at its spawn point while the real NPC still passes through the bot; three of five shadow-lifetime edges leaked (pickup leaves a permanent invisible collider, supersession orphans a duplicate, generation reset never unregisters and the K-ledger convergence oracle only checks retained shadows AFTER disposal clears them); and headless cannot resolve BSP collision assets at all, so doors and chests would still be walk-through. The frozen-shadow root was the SESSION LEAD's contract error (fact 3), not the implementer's. #330 stays OPEN, rewritten as the seven-point scope map the reviews produced — the honest overnight deliverable is that map, not a half-mechanism carrying new divergences. Suite 11,235 passed / 4 skipped / 0 failed (the withheld seam's two tests account for the delta from the implementation run's 11,237). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3aab05b0cc |
fix(streaming): derive the portal reveal window from the live streaming radii (#280)
The user watched far terrain visibly assemble after portal space exits.
The reveal gate was NOT missing a hold — Slice E's hold mechanism is
correct and already in place. The hold was measuring the wrong domain:
it opened at a hardcoded 3x3 landblock neighbourhood (~192 m) while the
visible world extends to the fog end (~2,189 m at the shipped High
preset, inside a 2,304 m Far window). An 11.4:1 ratio.
Retail's equivalent ratio is 1:1 BY CONSTRUCTION. `LScape` owns one
`mid_width x mid_width` array of `CLandBlock*` (`LScape::SetMidRadius`
@0x00504C00, `LScape::update_block` @0x005063A0), `mid_radius` is
assigned directly from the user's `Render.LandscapeDrawDistance`
preference (`SmartBox::SetRegion` @0x004531F0; values
`Render_LandscapeDrawDistance_Values` @0x007CA988 = {3,5,8,11,15,25},
default 8 — both byte-verified against the PDB-paired 2013 binary), and
that same square is simultaneously the prefetched set
(`LScape::PreFetchCells` @0x00505660), the drawn set (`block_draw_list`
over the same array), and the set the simulation blocks on
(`CellManager::blocking_for_cells`). There is no retail configuration in
which the client streams farther than it gates, because there is only
one number.
So the fix derives rather than duplicates. Four coupled parts, which is
why this is one commit and not four — D1 without D2 hangs the client and
D2 without D1 is dead code:
D1 `WorldRevealReadinessBarrier` takes a live `Func<StreamingRevealWindow>`
and stops being static: outdoor requires `FarRadius`, indoor still 0
(retail's `CEnvCell::PreFetchCells` @0x0052D1E0 arm). Read per
evaluation, never captured — the radii are runtime mutable through
Settings, and retail's answer to a mid-hold radius change is to reset,
re-radius, and re-arm the blocking prefetch at the NEW value
(`SmartBox::set_mid_radius` @0x00453180). `OutdoorNeighborhoodRadius`
is deleted; there is no constant left to drift.
D2 `StreamingController.IsRenderNeighborhoodResident` becomes tiered,
because acdream's loaded landscape is: inside `NearRadius`,
`IsNearTier && IsRenderReady`; out to `FarRadius`, `IsRenderReady` only.
Without this the fix cannot work at all — nothing outside the Near ring
is ever promoted, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever. Proof obligation P1 (a Far-tier
landblock genuinely satisfies `IsRenderReady`) is now a test driven
through the real `PublicationKind.Far` pipeline against a real
`LandblockSpawnAdapter`, not an inference.
D7 `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived
`indoor ? 0 : 1` and failed `invalid-readiness-shape` on any other
value, so changing the radius alone would have looked like "the fix
hangs the client". It is now a SHAPE invariant (`indoor => 0`,
`outdoor => >= 1`). Runtime does not own the graphical host's streaming
configuration and must not learn it; plumbing App radii into Runtime to
preserve the strict equality is exactly the assert-a-mechanism-that-does-
not-exist failure C5b was built to stop. Both non-graphical producers
keep emitting their centre-ring token and stay legal, annotated in place.
D6 `PhysicsEngine.IsNeighborhoodTerrainResident` rebuilt a full-map
`HashSet` on every call, every frame of every hold. At radius 1 that was
invisible; at radius 12 (625 ring members) it violates Slice I1's
0 B/resolve standard. Now an engine-owned scratch set, cleared in place;
measured at 0 bytes over 1,000 warmed radius-12 queries.
Also: the destination reservation opens at exactly the gate's radius and
reopens on the same generation when the radius changes mid-hold (retail
has one square for both, and no concept of prioritising an inner ring
differently). Composite warmup deliberately stays `NearRadius`-scoped —
the composite domain is entity-scoped and Far builds carry no entities,
so widening it would walk the outer window to warm nothing.
`ACDREAM_PROBE_REVEAL_RADIUS` is a measurement probe in a diagnostic
owner (CLAUDE.md rule 5) so the connected route can be run A/B on one
binary; it is NOT a user-facing prefetch knob, since a low setting would
reintroduce the decoupling this slice exists to close.
Register: AD-2 amended with the derived window, the two-tier split, and
the four new retail anchors. AP-149 FILED for the residual this does not
close — the outer ring accepts terrain-only publication where retail
requires LandBlockInfo and every building EnvCell, so a distant building
can still pop in at Far-ring distances. Do not let a later closeout
claim parity.
Docs: `ACDREAM_STREAM_RADIUS`'s CLAUDE.md description was wrong on every
clause (the default is unset, not 2; it forces `NearRadius`; it is
silently discarded by any Settings save) — corrected, since that is the
file every session reads. `reference_two_tier_streaming.md` corrected in
four ways, including "Far tier = terrain only": Far also publishes
terrain COLLISION, which is precisely what makes this fix viable.
#280's issue text had the right conclusion from a wrong premise (it
names a view-distance setting acdream does not have) — corrected, and
the missing Viewing Distance option filed separately as #326, with #327
(DDD progress readout) and #328 (hardcoded 5000 f far plane vs retail's
byte-verified 4000) filed alongside.
Expect LONGER holds and the "In Portal Space - Please Wait..." cue on
recalls MORE often. That is convergence toward retail, not away from it:
retail emits the byte-identical string for the whole duration of a
blocked prefetch and polls at 5 s intervals. The failure condition is
non-convergence, not duration.
Gates: Release build 0 errors. Complete suite 11,178 passed / 4 skipped
/ 0 failed, against a re-measured 11,142 / 4 / 0 baseline at
|
||
|
|
f8e55ba5e4 |
fix(physics): route local-player shadow presentation through SyncPose (#318, AP-145)
RuntimePlacementPresentationSink.TryPublishPlace previously published the local player's collision-shadow pose with a direct LocalPlayerShadowState.Set call — a plain cache write that never touched PhysicsEngine.ShadowObjects. Because LocalPlayerShadowSynchronizer.SyncPose's own dedup check compares against that same cache, the direct write could pre-seed the cache with the destination pose and cause the next real SyncPose call to see "nothing changed" and skip its own ShadowObjects publish — leaving the real collision shadow at the pre-teleport position until an unrelated movement tick forced a real publish. Fix: TryPublishPlace now calls _localPlayerShadowSync.SyncPose(..., force: true), the same publisher ordinary per-tick movement uses, so Place always drives a real ShadowObjects write before the cache updates. TryPublishWithdrawal carried the exact mirror asymmetry (a bare LocalPlayerShadowState.Clear with no ShadowObjects.Suspend, leaving a live phantom shadow row at the park's source cell for the whole park window — the #184 shape) and is fixed in the same commit, same one-call shape: _localPlayerShadowSync.Suspend(entity). The sink no longer holds a direct LocalPlayerShadowState reference; both halves route exclusively through the one synchronizer, which owns the cache internally. The single LocalPlayerShadowSynchronizer instance is now constructed in LivePresentationComposition (before the sink) and threaded through LivePresentationResult to SessionPlayerComposition, which no longer builds its own — this guarantees the sink's Place/Withdraw edge and ordinary per-tick movement publish through the exact same publisher and cache rather than two independent instances that could drift out of sync with each other. TryPublishPlace's xmldoc now states the behavioural nuance directly: routing through SyncPose means Place inherits SyncPose's own admission guard (IsHidden, cellId == 0, not-current-visible-projection), which the old direct .Set() call never consulted. Under those conditions SyncPose now calls Suspend instead of publishing — correct and symmetric, but new behaviour worth flagging at the call site, not just in a test comment. RuntimePlacementShadowCompositionTests.cs (#318) proves four facts against the real ShadowObjects registry, not the cache: a bare Place publishes a real row at the destination cell with the source cell's row gone; a subsequent ordinary per-tick Sync is then a correct no-op; a Place for a registered non-local-player entity leaves its row at the source cell untouched and never touches the player's cache (route 7 P4 — the fix lives entirely inside the pre-existing player-only gate); and Withdraw suspends the real registry row, not just the cache, with the retained (suspendable) registration surviving for a later restore. All four were sabotage-verified in both directions. RuntimeForcePositionRenderCommitTests.cs (B2) drives a real end-to-end accepted ForcePosition through RuntimeEntityObjectLifetime.TryApplyPosition and RuntimeAcceptedPositionDriveController.TryExecuteAcceptedLocalPosition against a live HostFixture, asserting both the committed render position AND a cell change that deliberately crosses out of the spawn's outdoor grid cell, so the cell assertion is independently falsifiable rather than riding along with the position assertion. Retires AP-145 (this fix) in docs/architecture/retail-divergence-register.md. AP-1 and AD-1 are untouched by this commit — they retire separately in the deletion-sweep commit that follows. Evidence chain: docs/research/2026-08-05-c5a-contract.md (the governing C5a slice contract), docs/research/2026-08-05-c5a-architecture-review.md (round 1, FAIL — three MAJORs: vacuous route-7 P4 test, unfixed Withdraw-side mirror asymmetry, non-driving B2 test), docs/research/2026-08-05-c5a-architecture-review-round2.md (round 2, PASS with two MINORs — an unfalsifiable B2 cell assertion and the undocumented SyncPose guard nuance, both fixed here). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e0f96a55bf |
fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival. Portalling worked before this change and works after it — this is not a bug fix, EXCEPT that it found and fixed one dead-code production bug. THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the accepted destination at Place time, but TryBeginPortalReveal already consumes that slot at Aim time — so the arm was 100% dead code and every real portal Place refused with host-token-unavailable. Found only because we refused to accept 7 skipped tests instead of chasing the count to zero. RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING: SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with flags 0x1012, followed by PlayerPositionUpdated. BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed here (ConstrainTo @0x0045418A) and velocity is zeroed (set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook runs AFTER placement (@0x004538AE). THE THREE-ROUND DEFECT CHAIN, HONESTLY: - Round 1 released the player at the pre-teleport position while the anim stream marched on — the contract wrongly assumed Place re-fires (process rule 1's third occurrence this campaign). - Round 2's fix inferred commit from a global PendingCount, which three non-committing paths also clear — making the SAME bug complete cleanly and silently. Strictly worse than round 1: round 1 at least tripped portal-complete-before-materialized. - Round 3 latches the commit where it actually happens (ReconcileAndAcknowledgePortal), keyed on reveal generation and teleport sequence, via TryConsumePortalCommit. Two of the three required regression tests landed and are sabotage-verified on both hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted / HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted). The third (force-arm-takes-the-slot) was judged unnecessary on review: with the inference gone, PendingCount is only a "don't ask yet" guard at both gates, so a force operation occupying or vacating the slot no longer changes an input the commit decision reads — the case collapses into what the landed test already discriminates. THE B2/P3 RESOLUTION: both round-2 reviews were right about different branches of the same synchronous call. RuntimePlacementProjectionSubscription .OnPlacement acknowledges the FIFO head only when TryApply returns true; a Place whose portal authority went stale (transit ended/superseded while parked) used to return false, wedging every later entity's placement receipt behind it forever. Both sinks (RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink) now acknowledge-and-ignore a stale-authority Place instead of refusing it. The regression test (RuntimePlacementPresentationSinkTests .PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had been asserting the old, wrong `false` behaviour; it now asserts and sabotage-verifies the fix. Also lands: AP-144 (register discipline — the portal movement-event send reuses the stricter UsePositionFromServer gate where retail's SendMovementEvent is the looser autonomy_level != 0 test, diverging only at level 1, currently unreachable), AP-145 + issue #318 (the local-player collision-shadow presentation write bypasses its own publisher's ShadowObjects write via a direct cache .Set(), self-healing only once dedup diverges — filed, not fixed, pending a composition test), AD-42 deleted (its last citation retired by the canonical portal arm), AD-2 updated (the wait-cue's trigger predicate now covers a second cause), and two documentation corrections: the enter_world misattribution (both call sites are in SmartBox::HandleCreateObject, only one in the player branch — portal arrival is TeleportPlayer, not enter_world) and the stale "local player never reaches this path" comment on the generic-remote-render-pose write. Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing weakened. STILL OWED: the connected two-client gate, with ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines actually appear in the capture — and explicitly NOT scored as covering issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects directly). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6dc7ba51ee |
feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f1c1f5aa6 |
feat(physics): C4 route 4b-2 — remote far snap through the canonical placement
Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for remotes onto 4b-1's drive controller and deletes both legacy far blocks, both duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The 4 m constant now exists exactly once. Teleport and cell-less stay legacy for 4b-3. Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating @0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8 regardless — the SetPositionError is discarded — so HandleReceivedPosition arms ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch. SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4. Non-commit outcomes still advance the body, because retail's SetPositionInternal @0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5). Without this a refused far snap froze the remote with an emptied queue. Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks withdrew the entity (InWorld=false, clock suspended, residency removed) and were never restorable, while Forget(restoreCancelledPark: true) runs for every accepted Position on every entity. The restorable decision now lives inside ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value RestoreParkWithdrawal actually restores at — against every live quiescence rather than one minimum-OperationId token. The three pre-snap fields are hoisted into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents passes restorableOnCancel: false explicitly; the plain unplaceable park is provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so a retained route-2 park cannot re-admit into a prefix that began quiescing during the park. CanAttemptDestination is retained as an OPTIMISATION only, with the two Core predicates it cannot reproduce written down at the pre-flight, plus the two properties that depend on it staying there. Four fix rounds and eight Opus reviews. The slice was fully green at 10,990, 10,997 and 11,004 while containing real defects — a frozen remote pinned as correct by its own test, a fallback that over-wrote on the exact retail paths that decline to store, and a park guard incomplete on two independent axes. Register: AP-137 (leftover classifications take AP-87's catch-up; states the cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied anyway, and the headless divergence), AP-138 (the refusable far placement), AP-136 narrowed to match the relocation. #309's acceptance steps rewritten — step 5 previously asserted a recovery the code does not perform — and gated on a new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken. Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline. The 10,973 figure recorded earlier was wrong and is corrected here. Connected gate outstanding: the two-client far-snap walk and #309. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b1e6fc637 |
fix(physics): #297 — keep the PWD bitfield live so PK status reaches the client
The user typed @pklite and then walked straight through other PKLite players. Root cause: ClientObject.PublicWeenieBitfield was written exactly once, from the 0xF745 CreateObject parse, and never refreshed. ACE's only PK-change message is PropertyInt.PlayerKillerStatus (134) over 0x02CE/0x02CD, which we parsed and stored into Properties.Ints[134] but never translated back into the bitfield — and ACE never re-sends a PublicWeenieDesc at all (EnqueueBroadcastUpdateObject has zero live callers), so that property is the ONLY signal a client can learn from. Both sides of the collision test read the frozen value, so CollisionExemption's "4c. both PKLite -> collide" rule could never fire. Retail's missing port: PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0 rewrites _bitfield in place — PK(4) -> (b & 0xfddfffff) | 0x20; PKLite(0x40) -> (b & 0xffdfffdf) | 0x2000000; Free(0x20) -> (b & 0xfdffffdf) | 0x200000; else b &= 0xfddfffdf. Mutually exclusive, verified byte-for-byte, with input values confirmed against retail's own PKStatusEnum (acclient.h:6412-6427), not just ACE's. Driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86. The fix rewrites the value at its source rather than patching consumers. Two review rounds were needed because the first pass missed that there are TWO snapshot stores: InboundPhysicsStateController keeps its own private _snapshots dictionary, and every untimestamped-field merge (ApplyAcceptedObjDesc and friends) reads `old` from THAT store, not from RuntimeEntityRecord.Snapshot. Refreshing only the active record left the target-side shadow flags correct until the remote's next equip or unequip — ACE broadcasts an ObjDesc on every one — at which point the appearance path rebuilt the registration from the frozen spawn and dropped the bit permanently. The regression test demanded by review is what surfaced that; it is verified discriminating (reverting gives Actual: 8 instead of 33554440). Five stores now hold this value, kept coherent from one source by two ObjectUpdated subscribers plus the appearance-rebuild path. The two shadow-flag writers are the same invalidation applied at the two edges that can invalidate it, not competing authorities — review enumerated every drift path and closed each. That coherence invariant is new as of this commit and is recorded as register row AP-134, with AP-133 as the precedent for filing a row when the danger is a future writer rather than current behaviour. Also corrects TS-23's retirement narrative, which claimed every mover-flags call site read the mover's "real" PK bits from 2026-07-30. The bits existed but their source was frozen, so that only became true here; the site enumeration also missed RuntimeSetPositionMoverPreparation, a seventh site that decodes the snapshot directly. Unblocks #298 (melee/missile admission needs the local player's own PKLite bit). Follow-ups filed: #300 (Properties.Ints[134] vs bitfield mirror gap), #301 (same defect class for radar blip colour and radar behaviour), #302 (a pre-existing PortalProjection allocation-assertion flake, 1 in 6, found while verifying this gate), #303 (LiveEntityPvpBitfieldSync is App-resident but Runtime-owned-state). Gates: complete Release solution 10,895 passed / 4 skipped / 0 failed (baseline 10,887 including #299). Adversarial + retail-conformance review PASS after one FAIL round. Every new test discrimination-verified by reverting the fix. Connected acceptance NOT run — needs a live two-client PKLite session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9966b53174 |
feat(physics): C4 route 2 — ForcePosition through the canonical placement
A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class
|
||
|
|
f24532adf3 |
fix(vfx): bind effects after canonical placement
C3c created graphical effect, projectile, and static-animation sidecars before Runtime finished the entity's first SetPosition. One-shot F754/F755 packets could be discarded, projectiles could adopt a cell-less body, and animated statics could compete for body ownership. Keep effects behind an exact-incarnation presentation barrier, retry projectile/static binding on the committed visibility edge, and keep effect cells synchronized with canonical rebuckets. User verified spell, recall, arrow, projectile, portal, and static presentation; 90 focused App tests and the Release build pass. |
||
|
|
529e0e9d88 |
feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing
|
||
|
|
f05ed5c3cd | feat(app): observe canonical placement receipts | ||
|
|
d6e8b60303 | fix(movement): invalidate burden on enchantment changes | ||
|
|
bb7b899bfe |
fix(physics): TS-23 - plumb real PK/PKLite/Impenetrable mover flags
Campaign P Slice P3 item 3. The wire parse (CreateObject's PublicWeenieDesc._bitfield), the decode (EntityCollisionFlagsExt. FromPwdBitfield), the per-GUID storage (ClientObjectTable. PublicWeenieBitfield), and the exemption logic (CollisionExemption. ShouldSkip) all already existed and were already correct -- every mover-flags call site just fed a GUID-prefix IsPlayer heuristic instead of the real per-entity PK/PKLite/Impenetrable state (retail OBJECTINFO::init 0x0050cf30 state |= 0x80/0x800/0x1000). Port: - EntityCollisionFlagsExt.ToMoverState translates the decoded PWD bit-space into the ObjectInfoState bit-space FindObjCollisions actually reads -- two different numberings that must not be confused. Deliberately does not translate IsPlayer (every call site already derives that correctly from its own GUID heuristic per #184 Slice 2b). - EntityCollisionFlagsExt.ResolveMoverPvpState is the one shared ClientObjectTable-backed lookup (guid -> ObjectInfoState), replacing what would otherwise have been three separate inline copies across GameWindow/LivePresentationComposition/RemoteTeleportController. - Threaded as a new optional moverPvpState parameter through RuntimeRemotePhysicsUpdater.Tick/TickHidden and RuntimeOrdinaryPhysicsUpdater.TryBegin (default None preserves every pre-P3 caller unchanged), and as PlayerMovementController.OwnPvpFlags for the local player's own two resolve call sites. - TS-23 section 12b: PlayerWeenie.JumpStaminaCost's pk parameter now reads the real PlayerKillerStatus(0x86)/LastPkAttackTimestamp(0x91) pair against retail's 20-second recency window (pkStatus in {4, 0x40} && (timestamp + 20.0) >= now), replacing the P1 hardcoded false. RuntimeMovementSkillState/Snapshot and LiveSessionEventRouter.RecomputePvpStatus push both the PWD bitfield and the PlayerKillerStatus pair reactively, riding the SAME ClientObject event triggers RecomputeBurden already uses. - A conformance test caught a genuine precision bug in the first PK-timer clock choice: DateTimeOffset.UtcNow's Unix-epoch seconds (~1.7 billion) loses ~128 seconds of precision in a 32-bit float, silently swallowing the entire 20-second window. Switched to Environment.TickCount64 (small, monotonic magnitude) -- also the more retail-plausible basis, since LastPkAttackTimestamp is itself a wire PropertyFloat and retail's Timer::cur_time is almost certainly a process/session-relative counter for the same precision reason, not an absolute epoch. Non-PK invariant (the acceptance criterion): an entity with no ClientObjectTable row, or a row whose PublicWeenieBitfield is null or 0, resolves to ObjectInfoState.None -- a no-op OR into moverFlags, bit-identical to every pre-P3 caller's hardcoded value. A dedicated test drives two real ClientObjectTable rows through CollisionExemption.ShouldSkip and confirms PK-vs-PK collides while PK-vs-non-PK and non-PK-vs-non-PK both stay exempt (walk through). Register: TS-23 retired (both the collision-flags and PK-timer halves); the stale "M2 combat must land TS-23" phase-gate note removed. dotnet build + dotnet test (Core.Tests 4008/2 skip, Runtime.Tests 425/0, App.Tests 3968/3 skip, complete solution build) all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dae5b1ea68 |
fix(physics): TS-46 - seed the sweep from the Setup's own sphere list
Campaign P Slice P3 item 1. Retail CPhysicsObj::transition (0x00512dc0) seeds the collision sweep from CPartArray::GetSphere (the Setup's own <=2-sphere list, each origin+radius scaled by m_scale) via SPHEREPATH::init_sphere (0x0050c670) -- not from a symmetric two-scalar (radius, height) capsule reconstruction. The human Setup 0x02000001's authored spheres are (0,0,0.475) r=.48 and (0,0,1.350) r=.48; the old reconstruction from (0.48, 1.835) produced (0,0,0.48) + (0,0,1.355), a 5 mm head-center offset the TS-46 register row documented as a residual. Port: - SpherePath.InitPath gains a sphere-list overload (ImmutableArray< FlatCollisionSphere>, scale) sharing a new InitPathCore with the existing (radius, height) overload, which is now the degenerate 2-scalar case of the same code -- byte-for-byte unchanged, so every captured-fixture replay (CellarUpTrajectoryReplayTests, DoorBugTrajectoryReplayTests, CellarLipWedgeTests) keeps passing unmodified. - PhysicsEngine.ResolveWithTransition gains optional sphereList/ sphereScale parameters; empty/default preserves the legacy scalar path for every pre-existing caller. - LiveEntityMotionRuntimeController.GetSetupMoverShape is a new sibling of GetSetupCylinder (left untouched) that resolves the Setup's own sphere list plus Setup-derived step-up/step-down (CPartArray::GetStepUpHeight/GetStepDownHeight, 0x005180d0/0x005180f0, x ObjScale, 0.4 m fallback matching the pre-existing literal). - Threaded through PlayerMovementController (both resolve call sites, new SphereList property set by PlayerModeController.ApplyStepHeights and the Headless world projection), RuntimeRemotePhysicsUpdater (Tick + TickHidden), and RuntimeOrdinaryPhysicsUpdater.TryBegin. Remote/ordinary step heights are now Setup-derived instead of a hardcoded 0.4f literal. Projectile and camera-probe sweeps are untouched (already single-sphere-exact). - PlayerModeController.ApplyStepHeights also now applies the x ObjScale multiply to the player's own step heights (previously only the remote/ordinary paths did), closing an adjacent gap the P3 research flagged. Ts46SphereListConformanceTests proves the sphere-list overload sees the exact dat spheres (not the reconstruction), that the scalar overload is unchanged, and that ResolveWithTransition's sphereList parameter actually drives the sweep (a decoy-scalar control pair using a head-height obstacle sphere). Register: TS-46 retired (both residuals it named are closed); header count corrected to 40 active TS rows. dotnet build + dotnet test (Core.Tests 3991/2 skip, Runtime.Tests 425/0, App.Tests 3968/3 skip, complete solution build) all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aa07baed42 |
feat(diag): #262 - wire the permanent [snap] login/teleport diagnostic (Campaign P P6)
PhysicsEngine.DiagnosticLog was never assigned in production, so the #111 [snap] apparatus (one line per entry-snap Resolve, low volume by design) was structurally silent - including on the Coldeve run-on-the-spot login. Wire it at session composition; a session reset constructs a fresh engine and re-wires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f6db964fd5 |
feat(interaction): Slice 4 - equipped-child world picking
A click on a remote character's wielded weapon reported nothing. The picker was already correct: RetailSelectionScene publishes every drawn part under its own live-entity server GUID and RetailWorldPicker returns the weapon as the polygon winner. The failure was downstream eligibility - WorldSelectionQuery required TryGetInteractionEligibleRecord, whose _visible set admits LiveEntityProjectionKind.World only, so the winning hit was discarded. Retail has no such gate. Render::GfxObjUnderSelectionRay @ 0x0054C740 accumulates each hit under the drawn part's own physics-object id (CPhysicsPart::get_physobj_id @ 0x0050D490), and CPhysicsPart::Draw @ 0x0050D7A0 admits any drawn part whose physobj id is nonzero. An equipped item is a first-class CPhysicsObj with its own id and part array (CPhysicsObj::add_child @ 0x0050F870 via CSetup::GetHoldingLocation @ 0x005213F0). There is no parent redirection and no wielded-specific rule, so a click on a wielded weapon returns THE WEAPON'S GUID. PositionState.WIELDED is distinct from IN_CONTAINER (acclient.h:6802), so container suppression never hid a wielded selection either. LiveEntityRuntime gains two scoped predicates: TryGetAttachedProjectedRecord (a current Attached projection that is spatially projected) and TryGetPickEligibleRecord (that arm plus today's World visible-set arm, with the same WorldEntity.Id staleness recheck). TryGetInteractionEligibleRecord and the _visible set are deliberately NOT widened - they feed radar, auto-target, sticky/MoveTo establishment, and CombatAttackTargetSource, and retail's radar has no wielded blips. A regression test asserts an attached child stays out of that set while picking admits it. Marker anchoring had the twin problem. SmartBox::GetObjectBoundingBox @ 0x00452E20 pushes the picked object's OWN m_position - which for a child is the frame CPhysicsObj::UpdateChild @ 0x00512D50 recomposes each tick as Frame::combine(parent part frame, holding frame) - and CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere by that object's own part-array scale. acdream stores the PARENT's root in the child projection's Position/Rotation because the child's MeshRefs are parent-relative, which put the vivid brackets at the wielder's feet. The composed child root is already published per frame to EntityEffectPoseRegistry by EquippedChildRenderController.PublishChildPose, so selection now borrows it through an injected Func<uint, Matrix4x4?> wired in LivePresentationComposition beside the existing selection-sphere hook. There is no parent fallback: a child with no published composed root has no live frame this tick and no sphere. Its part-array scale comes from the spawn record, the same source EquippedChildRenderController.TryRealize reads, because an Attached WorldEntity carries the parent-derived pose rather than its own ObjScale. The sr_Use branch of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 guards ItemHolder::UseObject with `found->pwd._wielderID != SmartBox::player_id` at 0x004E5BE9 while still selecting and flashing. Equipped-child picking makes that click reachable, so the gate ships with it as IWorldSelectionQuery.IsWieldedByPlayer. CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive, so the pulse lights the clicked object's own part array only - clicking a weapon never flashes its wielder. That follows from routing the pulse identity through the same predicate. RetailWorldPicker, RetailSelectionScene, WbDrawDispatcher, and EquippedChildRenderController are untouched, as are all wire and physics paths. The slice REMOVES an undocumented deviation (Attached projections excluded from pick eligibility versus retail's part-id pick) and introduces none, so no retail-divergence-register row is owed in either direction. Gates: dotnet build green; AcDream.App.Tests 3,951 passed / 3 skipped; complete Release solution 9,783 passed / 5 skipped; tools\run-connected-world-lifecycle-gate.ps1 RESULT=PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7a0227c12e |
feat(render): Vulkan campaign V11 step 3 — drop the GL packages and shaders
Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8a7a0837e1 |
feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
844cf092a1 |
feat(render): Campaign V slice V11 commit 1 - delete ImGui, Studio, and the DevTools frontend
The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio (src/AcDream.App/Studio), and the DevToolsFramePresenter/ SettingsDevToolsCompositionPhase ImGui composition machinery are removed. Vulkan never composed a DevTools frontend (DevToolsEnabled already forced false whenever the backend was Vulkan); this commit makes that permanent by deleting the only implementation rather than leaving a dead branch behind. What moved: Studio/SampleData.cs is a live production dependency (InteractionRetainedUiComposition's character-sheet fallback, plus three UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs, namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid and icon constants had zero callers left once the Studio fixture provider that used them was deleted). What survives as backend-neutral seams, per the tests that still exercise them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs, now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget adapter is deleted), and IDevToolsGameplayCommands in GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a documented no-op instead of forwarding to the deleted presenter). A follow-up re-homes Settings/Debug onto the retained UI through IPanelRenderer; until then keybind remapping falls back to editing keybinds.json. DevToolsEnabled is now `private const bool DevToolsEnabled = false`. RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext for the optional debug-utils extensions; Program.cs now logs one line when ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is Vulkan-only now. Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui package refs), src/AcDream.App/Studio (minus SampleData.cs), DevToolsFramePresenter.cs and everything only it constructed (ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory, DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication, SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger stage and its DevTools-typed fields on IngressShutdownRoots/ RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools composition branch, its _vitalsVm/_debugVm/_devToolsComposition/ _devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools .DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs are gone with it. Build green; complete Release solution suite 8,830 / 5 skips (App Tests 4,097/3 skips run standalone - one #250-family zero-allocation test flakes under the full parallel `dotnet test AcDream.slnx` run, a pre-existing, documented class unrelated to this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00e1b32177 |
perf(render): Campaign V slice V8 commit 1 - the Vulkan arm gets an instrument
V8 is the performance gate, and it opened on a plain fact: the Vulkan arm
emitted no [frame-prof] line at all. V6h wired it to
NullRenderFrameGpuMeasurement, whose BeginFrame does nothing - and that method
is the ONLY caller of FrameProfiler.FrameBoundary. So a Vulkan run produced no
CPU frame distribution, no allocation-per-frame column, no frame-history CSV
and no GPU sample. The campaign's own performance vehicle,
tools/run-connected-r6-soak.ps1, waits on [frame-prof] boundaries to time its
samples, so it could not be pointed at the backend V8 exists to judge. You
cannot measure what you have not instrumented, so the instrument lands first.
The bracket is deliberately identical on both arms. On GL,
FrameProfilerGpuMeasurement begins a TimeElapsed query at BeginFrame and ends
it at EndFrame, spanning resource preparation, the world scene and private
presentation, and NOT the swapchain present. VulkanFrameGpuMeasurement opens
and closes a Vulkan timestamp scope at exactly those two points. Two
differently-bracketed numbers in one comparison table would have been worse
than reporting none.
Vulkan timestamps resolve two or three frames late, so the sample carries the
profiler frame index that ISSUED it rather than being credited to the frame
that happened to read it - the pairing GpuFrameTimer already performs
internally on GL. VulkanGpuDevice opens the whole-frame scope tagged with that
index, reads the previous use of the slot back in BeginFrameResources BEFORE
the tag is overwritten, and hands completed (tag, microseconds) pairs to the
adapter through a bounded queue that never blocks.
VulkanGpuTimerPool gains TryTakeResolved, which consumes the value it reports.
TryResolve deliberately reports the last known measurement forever, which is
right for a diagnostic readout and wrong for a percentile: counting one
measurement into the distribution twice is how an instrument flatters itself.
FrameProfiler gains a GL-free FrameBoundary() overload and RecordGpuSample.
Every other line of its bookkeeping - the CPU delta, the per-thread allocation
delta, the stage buffers, the history row, the five-second report - is the same
code the GL arm runs. The GL path is byte-for-byte unchanged in behaviour:
FrameBoundary(GL) still owns and creates the query ring.
Gates: Release build green. App tests 4,152 passed / 3 skipped, exactly the
pre-slice baseline. Strict GL offline pixel gate against
|
||
|
|
ad5f8b68dc |
fix(render): Campaign V slice V7 commit 1 - world anisotropy, and the sky's second clock
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same stop on the same instrument now measures 9.05%, and the two populations these address are gone from the difference map rather than merely smaller. 1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE DEVICE MAXIMUM ON GL. RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and the only IWorldTextureArray the Vulkan arm ever constructs -- registered its clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler) set it again, which is the one that actually wins. V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that draws through these arrays is the next slice, and it is the one that can gate a filtering change visually." That slice was V6j, the gate is V7, and this is it. Retail settles the question rather than the GL arm settling it. RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at 0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's reported cap, not a setting -- so "as much anisotropy as this device has" is retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from retail as well as from the shipping backend. No divergence-register row is owed in either direction: this retires a Vulkan-only gap and lands on retail's value. The fix asks for a ceiling rather than reading a limit back, because the pinned RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It does not need one: VulkanGpuSampler already clamps MaxAnisotropy to VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at least 16 wherever the samplerAnisotropy feature is supported -- which this backend requires -- and 16 is where every desktop driver caps. The request and the GL arm's read therefore land on the same number. What it was worth, from the difference map at the same stop: the roof shingles of both Holtburg cottages, which had been dense hatching across the whole surface, and the stone courses of the near building are now black. Measured as high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to 1.023; every other textured region in the frame is between 0.99 and 1.02. Grazing-angle surfaces are where anisotropy is the whole difference, which is why a roof was the loudest thing in the frame. 2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE. ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth date, which chooses the day group, the keyframe and the sun angle. The cloud sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against DateTime.UtcNow minus its own construction time, by design, because retail's clouds drift with real time regardless of the date. Two launches minutes apart therefore cannot agree about where the clouds are no matter what the route does, and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows. ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds -> SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall clock, so nothing a user or the offline gate sees changes. The differential gate forces it on both launches alongside MSAA and the day group; the offline gate keeps its top-280 mask, because a same-commit GL pair still has the sun to disagree about. This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a workaround: it is one input to a UV offset, it is off by default, and no shipping path reads it. The alternative on the table was -MaskTopPixels, which would have permanently blinded the campaign's strictest instrument to the entire sky -- one of the five surfaces the offline gate already cannot see. Rows 0-32 of the Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up there is roof and portal rather than cloud. WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the distant treeline, the player and the NPCs, and the animated portal. The portal is phase and expected. The treeline is not filtering -- sharpness now matches within 5% and a shift search finds no sub-pixel offset -- and the two runs entered the world at different last-logout positions (0xC95B0001 versus 0x09040008), so the far-tier streaming history differed. That is the next thing to prove or refute. Gates. Release build green. App tests 4,133 passed / 3 skipped against the 4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31 band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero validation errors, zero warnings. Full three-stop differential recorded at artifacts/v7-diff-c1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59c6b2ae94 |
feat(render): Campaign V slice V6m commit 1 - portal space draws on Vulkan
PortalTunnelPresentation was the last raw-GL world-adjacent renderer. It now
draws on both arms, and the composition that used to hand the Vulkan arm a
portal-less teleport presentation is gone with it.
Nothing about the scene changed. Same synthetic DAT Setup resolved through the
same client-enum mapping, same 40 fps CSequence, same retail rotation cadence,
same distant light, drawn through the same already-dual-arm WbDrawDispatcher.
What forked is only where the draw is recorded:
* GL keeps its GLStateScope, its viewport/scissor/depth/cull/blend statements
and its depth-only glClear, untouched.
* The RHI arm opens a backbuffer pass of its own and publishes it on
IWorldPassScope for the span of the draw - the shape V6l gave the two
offscreen viewports, and required for the same reason: the dispatcher's RHI
arm borrows its pass rather than opening one. Publication comes after
BeginPass and before UploadRetailLight, because publishing resets the
frame-global sections and this scene wants its own light, not the world's.
The one substantive decision is the pass's COLOUR load op, and it is a Clear
rather than a Load. Retail preserves the colour target and only clears depth
(UIViewportObject::DrawContent @ 0x006950A5 -> Clear(4) = D3DCLEAR_ZBUFFER), and
so does the GL arm. A Vulkan pass cannot inherit an image the way a bound
framebuffer can: under MSAA the frame's world pass RESOLVES into the swapchain
image and stores DontCare into the multisampled scratch, so a second
multisampled pass declaring Load would load undefined contents - plan section
5.5.12 item 5, the same hazard that merged the clear into the world pass.
Re-clearing is exact rather than approximate because of an invariant the frame
graph already enforces. RenderFrameFoundation.PortalViewportVisible and this
scene's IsVisible are the same value, read once at the top of the frame, and
WorldSceneRenderer returns without drawing when it is set. So whenever portal
space draws, the backbuffer holds exactly the opaque black
SceneTool::BeginScene @ 0x0043DAD0 establishes and nothing else, and clearing to
that same black changes no pixel. The alternative - a single-sampled Load pass
over the resolved image - would have been both a silent MSAA divergence and
invalid, since the backbuffer's depth attachment is multisampled.
The pass takes IWorldPassScope.SampleCount, so WbDrawDispatcher's sample-count
pipeline variants (V6l) select the backbuffer set, and depth matches the
attachment.
CreateRequired becomes internal: its two new seams are internal RHI contracts
and composition is its only caller. The TYPE keeps its visibility - plan section
7.1 rule 3.
Gates. Release build green. App tests 4,132 / 3 skips against the 4,129
baseline (three new: the retail black constant, the RHI arm's composition
precondition, and the both-arms composition assertion). Complete Release suite
9,195 / 5; one AcDream.Content failure in the solution-wide run that passes
124/124 rerun alone - the documented rerun-singly flake class, not carried
forward as a claim. Strict GL offline pixel gate against
|
||
|
|
2e8b8b91ad |
feat(render): Campaign V slice V6l commit 3 - the offscreen viewports draw on Vulkan
Amendment 3 of three: the paperdoll and creature-appraisal views render on the
Vulkan arm. Plan section 5.5.16 defect 3 named two backend fixes as the
precondition; both are here, and running it found two more the note could not
have known about.
Fix 1: a layered sampled view per render target. An ATTACHMENT view must be
VK_IMAGE_VIEW_TYPE_2D and the global texture table's descriptor array is
sampler2DArray, so the attachment view cannot legally be registered into it -
section 5.5.7 recorded that as invalid usage rather than a mismatch that samples
oddly, and V6k made RegisterTexture refuse it loudly and name this fix.
VulkanGpuTexture now creates a SECOND, layered view over the same image for a
colour render target: one image, one allocation, two ways of looking at it,
legal without any creation flag. SampledView is what the table registers for
every texture, so the question disappears rather than being answered.
Fix 2: sample-count pipeline variants for WbDrawDispatcher. Vulkan requires a
pipeline's rasterizationSamples to equal the pass it draws in, and this
dispatcher draws in two passes with different counts - the multisampled
backbuffer world pass and the single-sampled offscreen target, which the
contract fixes at one sample. Its five pipelines became a MeshPipelineSet with
two instances, selected at bind time from the live pass rather than from the
scope, which is the same shape section 5.5.8 gave the depth-format problem. When
the backbuffer is single-sampled the two sets are one object, so nothing is
built twice and nothing is freed twice. The offscreen target's DEPTH attachment
also had to take the device's own combined depth/stencil format rather than the
contract enum's literal D24_UNORM_S8_UINT: a pipeline bakes one depth/stencil
format under dynamic rendering and the same pipelines draw in both passes, so a
second format would make one of the two undefined.
Fix 3, which running it found: entity APPEARANCE composites were still
bindless-only, so no entity with a palette override could be drawn on the Vulkan
arm at all - the doll being one, and every creature and player besides. The
backend that serves it has existed since V6i-2 and had no production consumer;
it has one now. TextureCache builds the composite cache on both arms, and
EnsureCompositeTexturesAvailable stops asking about bindless. Nothing about the
cache itself changed: the sharing, the bounded unowned LRU, the metered upload
budget and the retirement fence were already backend-neutral.
Fix 4, which the first successful capture found: the doll rendered upside down.
UiViewport has flipped V since V4a because a GL framebuffer's origin is
bottom-left, so its colour texture samples bottom-up. A Vulkan image's origin is
top-left and the backend's negative viewport height stores the rendered image
that way round, so the same flip stands the doll on its head. That is a property
of the backend that made the texture, not of the widget that draws it, so
IUiViewportRenderer answers TextureIsBottomUp and UiViewport asks. The line this
replaces had predicted exactly this failure since it was written.
The seam. WbDrawDispatcher's RHI arm borrows its pass from IWorldPassScope
rather than opening one, so a viewport that opens a pass of its own has to
publish it there for the span of the draw. Publish is on the interface now for
that. It does not nest: the world phase has closed its own pass by the time
private presentation runs, which is where these viewports have always drawn.
Gates. Release build green. App tests 4,129/3 skips; complete Release suite
9,192/5 (one solution-wide run reported a single App failure that did not
reproduce in the App suite alone or in a second solution-wide run - the
documented rerun-singly flake class; the failing test name was not surfaced by
the runner and is not carried forward as a claim). Strict GL offline pixel gate
against
|
||
|
|
eced67d038 |
feat(render): Campaign V slice V6l commit 2 - the portal mask draws on Vulkan
Contract amendment 2 of three, and V4g's remaining half behind it. Plan section
5.5.16 defect 2: PortalDepthMaskRenderer's two-pass punch (#117) is built on
glStencilFunc/glStencilOp/glStencilMask, GpuPipelineDescription carried no
stencil state at all, and nothing else can express it - so the renderer stayed
raw GL, invisible to the Vulkan arm, and V4g's "stencil/depth-mask pipelines"
row could not be written.
The amendment splits the way core Vulkan 1.3 splits. The ENABLE and the
attachment intent are baked: GpuPipelineDescription.StencilTest, false by
default so no pipeline in the tree changed. The per-draw compare, three outcome
ops, reference and both masks are a GpuStencilState that the pipeline carries as
a DEFAULT and IGpuPassEncoder.SetStencil overrides - exactly the split cull
mode, front face and depth write already have, and exactly what
VK_DYNAMIC_STATE_STENCIL_OP/_COMPARE_MASK/_WRITE_MASK/_REFERENCE make dynamic.
The four stencil dynamic states are declared ONLY by a pipeline that tests
stencil: declaring a dynamic state obliges every draw with the pipeline to have
set it, so adding them unconditionally would make every existing pipeline depend
on a call none of them make. GpuStencilOp carries three values because the punch
uses three - Replace marks, Equal gates, Zero self-cleans - and a fourth would
be a facility with no consumer.
The arm. Three pipelines, not one, because depth COMPARE is not dynamic in the
contract and the punch's two passes differ in it: mark tests LEQUAL and writes
no depth, punch tests ALWAYS and writes, seal is ALWAYS + write with no stencil.
All three write no colour, which is what retail's "COLOR-INVISIBLE triangle fan"
means. The fan is expanded to a triangle LIST on the CPU - the contract has no
fan topology and Vulkan's is not portable - which is exact: triangle i is
(v0, v[i+1], v[i+2]), the same triangles in the same order.
portal_depth.{vert,frag} is a new committed shader pair, and this is the ONE
renderer in the campaign whose two arms do not share a source. Its clip planes
have to travel in the TerrainClip uniform block at binding 2, which is already
precisely this shape and already read by terrain_modern.vert and sky.vert - but
on GL that binding is held globally by ClipFrame for terrain, so a portal draw
that rebound it would leave every later terrain draw in the frame reading the
wrong region. The GL arm therefore keeps its inline program.
PortalDepthShaderParityTests is the tripwire: retail's far-Z constant
(0.99999988, from DrawPortalPolyInternal 0x0059bc90), #129's capped mark-bias
expression and the eight-half-plane loop are asserted to appear in both. Both
are deleted at V11. 9/10 shader pairs now compile to SPIR-V.
Two GL-side gaps closed while the state was being extended, both of section 7.1
rule 1's class rather than new work. GlAmbientCapabilityState now saves and
restores the stencil test, function, ops and both masks - the portal punch draws
mid-frame among renderers that are still raw GL and assume the test is off - and
the COLOUR MASK, which had no consumer until a colour-invisible pipeline existed
and whose absence would have blacked out every raw-GL renderer after such a
pass.
PortalTunnelPresentation was re-read and confirmed as V6k left it: it clears
depth and draws into the active viewport, binds no framebuffer of its own, and
needs no port for section 5.4's sake. It remains unported on the Vulkan arm -
the composition uses NullLocalPlayerTeleportPresentation there - which is an
absence on the V7 list, not a defect.
Gates. Release build green. App tests 4,129/3 skips; complete Release suite
9,192/5 (one solution-wide run reported a single App failure that did not
reproduce in two subsequent runs, solution-wide or alone - the documented
rerun-singly flake class). Strict GL offline pixel gate against
|
||
|
|
b1ad1d481b |
feat(render): Campaign V slice V6l commit 1 - particles draw on Vulkan
Contract amendment 1 of three, and V4e's content behind it. Plan section 5.5.16
recorded that both particle pipelines draw with per-instance VERTEX attributes
and that the pinned contract could express instanced DRAWING but not instanced
vertex INPUT: one stride, no divisor, one buffer at VertexInputRate.VERTEX. That
is what stopped V4e. This takes the reviewed option (i) - a second vertex
binding with a per-instance rate.
The amendment. GpuVertexLayout grows a per-binding notion (binding index,
stride, input rate) and GpuVertexAttribute names the binding it is fed from,
defaulting to 0; IGpuPassEncoder.BindVertexBuffer takes a binding index. Every
layout written before this slice keeps its exact meaning through
GpuVertexLayout.Interleaved, which is one vertex-rate binding 0 - and
GpuContractTests asserts that as a requirement rather than trusting it. Both
backends carry the rate natively and at no cost: VK_VERTEX_INPUT_RATE_INSTANCE
on the pipeline, glVertexAttribDivisor recorded once into the pipeline's VAO
where it survives every later attribute rebind.
GpuVertexFormat.UInt1 comes with it, and is necessary to it: particle.vert
declares `layout(location = 6) in uint aTextureIndex` and the amendment's whole
premise is that no shader is edited. Same kind-distinction UByte4UInt was added
for at V4d - GL needs glVertexAttribIPointer, Vulkan needs R32_UINT, and the
float path would reinterpret the value's bits rather than approximate them.
Options (ii) and (iii) were rejected on the record: all ten storage bindings are
spoken for and reusing binding 0 would have the GL particle draw clobber
WbDrawDispatcher's instance array mid-frame (section 5.5.8's hazard in its GL
form); CPU-expanding instances is 5x billboard bandwidth and does not scale to
mesh particles at all.
The arm. ParticleRenderer.Rhi.cs is a SECOND arm per section 5.5.6, not a
replacement - every GL statement in the sibling file is the one it always
issued. Five pipelines replace the imperative glBlendFunc switch (two billboard
blends, three mesh blends) because core Vulkan 1.3 does not make blend dynamic.
The per-flight VAO/VBO pool disappears because every ring allocation inside a
frame is already distinct memory that lives until the frame retires. The
binding-9 table is not bound at all - the device owns the table and the encoder
binds set 2. The pass is BORROWED from IWorldPassScope. Depth tests but does not
write, compare is Less and alpha-to-coverage is off, which is the ambient GL
state particles have always drawn under rather than a choice. Everything above
the submission seam - emitter iteration, retail distance ordering, the
deferred-alpha handoff, billboard axis construction, blend resolution - is the
same CPU code on both arms.
The first Vulkan particle frame threw rather than drew, which is the second
defect of the compiles-clean class this slice found by running:
TextureCache.AcquireParticleTexture is bindless-only, so the standalone particle
texture cache did not exist on a backend without GL. It exists on both arms now.
Everything about it that matters - sharing equivalent surfaces between emitter
owners, the bounded unowned LRU, retirement behind the frame-flight fence - is
already backend-neutral; only how one entry is created and destroyed differs,
which is what IStandaloneBindlessTextureBackend is for. The RHI arm creates the
image through IGpuDevice.CreateTexture with a real sampler and releases the
table slot before the image, which is the GL arm's order and for the same
reason. The composite cache stays GL-only: it serves entity appearance, not
particles.
The durability fix V6k earned. That slice found the sky declaring a 32-byte
stride against a 36-byte AcDream.Core.Terrain.Vertex - the record carries a
TerrainLayer no sky attribute names - and noted that every .Rhi.cs arm restates
a CPU record's footprint from memory while only sky had a test.
RhiVertexLayoutStrideTests is that test for the rest: world mesh, terrain, sky,
retained-UI sprite, debug line, and both particle bindings, each asserted
against the record or the producer's own float count, plus two sweeps over all
seven for attributes that reach past their stride or name an undeclared binding.
Four private layouts became internal to be assertable; nothing else about them
moved.
Gates. Release build green. App tests 4,121/3 skips (4,109 baseline plus three
contract tests and nine layout tests); complete Release suite 9,184/5. Strict GL
offline pixel gate against
|
||
|
|
eb7e6b4e5c |
feat(render): Campaign V slice V6k commit 2 - the viewports name their own target, and section 5.4 is discharged
V4g's first half, and the V7 blocker section 5.4 named. What moved. PrivateEntityViewportRenderer - the paperdoll and creature-appraisal viewports - stops hand-rolling an FBO, a colour texture and a depth renderbuffer and asks the device for an IGpuRenderTarget. The pass it opens DECLARES that target rather than binding one behind the RHI's back, and the colour attachment is registered into the global texture table through RegisterTexture like any other texture. Render() returns the UiTextureTableHandle the retained UI already speaks instead of a raw GL name. That deletes the V4a pre-approved transitional seam. GlGpuDevice's RegisterExternalColorTexture / TryResolveExternalColorTexture existed so the UI could blit a texture whose owner the RHI knew nothing about; plan section 7.1's final paragraph gave them exactly this slice as their end, and both are gone along with the GlGpuDevice casts in RetailPaperdollFrameView and RetailCreatureAppraisalFrameView. Those two views are now backend-neutral: they decode a handle instead of registering one. Section 5.4, stated precisely, because the answer is not what the section predicts. The divergence it describes - GL's BeginPass refusing to bind framebuffer 0 for a null target - is NOT on the tree and has not been since the V4c revert at |
||
|
|
22aa2edc65 |
feat(render): Campaign V slice V6k commit 1 - the sky draws on Vulkan
V4f's content, landed as a SECOND arm per section 5.5.6: GL keeps its raw world
path through to V10 and the RHI world path ships on Vulkan. Every GL statement in
SkyRenderer is the one it always issued; the encoder arm lives in SkyRenderer.Rhi.cs
and runs only when there is no GL context.
What it produces. ACDREAM_RENDER_BACKEND=vulkan renders the sky: the dome
quadrants, the horizon band, the cloud sheet and the fog gradient, in the same
place and the same colours as the GL capture of the same scene (within a few
units on the channels sampled, which is the day-fraction drift between two
launches). Section 5.5.15's first V7 defect - "the sky is flat fog" - is closed.
Three things differ from the GL arm, each because Vulkan bakes what GL sets. The
per-submesh blend function becomes two PIPELINES, additive for sun/moon/stars and
straight alpha for everything else, because core Vulkan 1.3 does not make blend
dynamic. The SkyParams block becomes a ring slice taken per draw rather than one
buffer rewritten per draw, because a descriptor's contents are read at execution
time, not record time. And the pass is borrowed from IWorldPassScope, because the
frame's one backbuffer pass resolves and a second pass could not load what it
left.
The sky is the first Vulkan consumer of set 1 binding 4. Section 5.5.8 recorded
that UniformSkyParams was missing from the uniform set layout and V6i-2 added it;
until now nothing had ever bound it.
The stride bug, which is the fourth of its class this campaign. The first Vulkan
sky frame drew the dome as a field of blue-white noise. The RHI vertex layout
declared a 32-byte stride - position, normal, texcoord, exactly what sky.vert
reads - while AcDream.Core.Terrain.Vertex is 36 bytes: it carries a fourth
member, TerrainLayer, that no sky attribute names and that the GL arm never
described to a glVertexAttribPointer but did count, because it says
sizeof(Vertex). Nothing else in the frame looked wrong, no validation rule was
violated, and the offline pixel gate masks the sky band, so only a side-by-side
capture found it. SkyVertexLayoutTests now asserts the REQUIREMENT - the stride
is the uploaded record's footprint - rather than today's number.
The last interim handle table is gone. V4t retired the private
GlBindlessHandleTable in WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer
and ParticleRenderer and deliberately left the sky's, because the sky is the one
world path that mints its own resident handles from TextureCache's raw GL texture
names rather than interning someone else's. It now registers those handles
through V4t's RegisterWorldTextureHandle seam instead, which is the same
mechanical change the other four took, and the class and its tests are deleted
because nothing else ever used them.
TextureCache gains RegisterWorldSurface(surfaceId, repeat), the sky's RHI texture
source: the same DecodeFromDats the GL path uses, created through
IGpuDevice.CreateTexture and paired with a real sampler object rather than baked
into a bindless handle. Keyed by (surface, wrap) for the same reason the GL arm
keys its handles that way - a table entry is a combined image sampler, so the
dome sampled CLAMP_TO_EDGE and a scrolling cloud sheet sampled REPEAT are two
entries over one decoded texture.
Gates. Release build green. App tests 4,109 passed / 3 skipped - the 4,112
baseline less the six GlBindlessHandleTable tests that went with the class, plus
three vertex-layout tests. Strict GL offline pixel gate against
|
||
|
|
f84eef3256 |
feat(render): Campaign V slice V6j commit 2 - Dereth draws on Vulkan
The three world renderers' submission arms, both pass executors, and the
composition that reaches them. This is the unit three predecessors stopped at.
What it produces. ACDREAM_RENDER_BACKEND=vulkan on the offline scene renders
terrain with blended textures and road overlays, the water edge, static world
meshes, procedural scenery, and the complete retained UI - the same frame the GL
pixel gate captures, from the same camera, minus the sky. artifacts/v6j-vk2.
The shape, and why it is not V4c's. Section 5.5.6 chose option (B) after NVIDIA
rendered the V4c binary 10/10 where AMD's GL stack did not: GL keeps its raw
world path through to V10 as a documented fork confined to the submission seam,
and the RHI world path ships on Vulkan. So V4c's and V4d-2's content returns as a
SECOND arm rather than a replacement. The GL arm issues the same GL statements in
the same order against the same objects; the encoder arm lives in three .Rhi.cs
partials and is entered by one branch per submission site.
Three differences from V4c, each because the tree moved under it. There is no
binding-9 texture table - V4t put the slot on the device and Vulkan binds set 2,
so the arm that used to intern bindless handles simply has nothing to do. The
pipelines carry the device's sample count rather than 1, because Vulkan requires
rasterizationSamples to match the pass and alpha-to-coverage is a no-op at one
sample. And no renderer opens a pass.
That last one is structural, not tidiness. Under MSAA the frame's one backbuffer
pass resolves into the swapchain image and stores DONT_CARE into the multisampled
scratch, so a second pass declaring Load would load undefined contents; the
backend also permits one open pass per frame. VulkanWorldScenePhase therefore
opens the pass, publishes the encoder on VulkanWorldPassScope for exactly the
span of the inner WorldSceneRenderer, and every renderer borrows it.
Three sections are frame-global on GL and cannot be on Vulkan: the SceneLighting
UBO, the per-cell clip regions, and the terrain clip block. GL binds each to a
global binding point and every consumer inherits it. Vulkan binds a descriptor
set per draw, and a renderer's own binds are what select the scope those sections
must land in - so their writers PUBLISH into WorldFrameSections and each renderer
binds them inside the pass, after its own binds. SceneLightingUboBinding's
per-flight-slot buffer pool disappears with it: a ring allocation is already
distinct memory that lives until the frame retires, which is the property the
pool existed to provide.
Both pass executors became backend-neutral rather than gaining twins. Everything
they do is delegation to a renderer except four concerns - the clip-frame
publication, the doorway scissor, gl_ClipDistance enablement, and retail's
interior depth clear - so those four move behind IWorldPassSurface and retail's
ordering, which is what these classes are actually for, is written once. The GL
implementation issues the statements the executors used to issue inline.
Clip distances are no-ops on the Vulkan arm, and that is safe rather than a
divergence: Vulkan activates every element the shader declares, and all three
world vertex shaders already write 1.0 into every slot past the active count.
The interior depth clear becomes vkCmdClearAttachments, reached through the scope
so the pinned contract stays frozen and the backend-only verb stays in the
backend. The hook for it was already committed at V6i-3 with a cref to a type
that did not exist yet; it exists now.
The collision-wireframe DebugLineRenderer is composed as null on the Vulkan arm.
DrawAndPublish flushes it INSIDE the world phase and it opens its own pass, which
the one-pass rule forbids. The toggle is DevTools-only and DevTools is not
composed there, so nothing is lost - composing it would throw on the first
wireframe frame rather than silently misdraw.
Two seams widened rather than invented. GameWindowGraphics answers whether the
backend has a world-pass seam, because the three composition phases that need it
already borrow that handle and "does this backend work that way" is what the type
exists to answer. And MeshSourceReady replaces the anyVao != 0 gate with the same
question in backend-neutral form - V6i-3 published HasStores for exactly this -
so the predicate evaluates identically on GL.
What is NOT here, and is expected. Sky and weather are still raw GL (V4f), so the
Vulkan frame's sky is the atmosphere fog clear. Particles (V4e), the paperdoll and
appraisal viewports and the portal depth mask (V4g) likewise. The executors
already accepted all of them as absent.
Gates. Release build green. App tests 4,112 passed / 3 skipped, the unchanged
baseline; complete Release suite 9,175 / 5. Strict GL offline pixel gate against
|
||
|
|
887de4aec2 |
feat(render): Campaign V slice V6i-3 commit 2 — the Vulkan frame gets a world pass, and descriptors bind at draw time
Two structural prerequisites for the world renderers' submission arms. Both are in Gpu/Vk only; the GL backend executes not one changed statement, and the frame this commit produces is bit-identical to the one before it. 1. The clear merges into the world pass (plan §5.5.12 item 5). V6h gave the clear phase a backbuffer pass of its own: clear, resolve, close. Under MSAA that is a trap for whatever comes next. A multisampled backbuffer pass renders into a scratch image and RESOLVES it into the acquired swapchain image, and the scratch's store op is DONT_CARE — so a world pass that followed and declared Load would load undefined contents and lose the clear entirely. The world renderers cannot work around it by each opening their own pass, for the same reason: every pass after the first would load a discarded scratch. So the clear phase now computes the same RenderFrameFoundation from the same world clock and weather owners and publishes only the COLOUR, through VulkanBackbufferClearState; VulkanWorldScenePhase opens the one backbuffer pass and clears as its load op, with Store=Resolve when the backbuffer is multisampled. That makes it the frame's one clear and its one resolve. The retained UI's pass is single-sampled and targets the swapchain image directly, so it composites over the resolved result exactly as it did. The clear stays unconditional because the frame graph makes it so rather than because anything asserts it: RenderFrameOrchestrator runs resource preparation, then the world phase, then private presentation, with no branch between. A frame with no world still opens the pass and leaves a cleared backbuffer — which is precisely the frame captured below, since nothing draws into the pass yet. 2. Descriptor sets bind at DRAW time, not at bind time. V6i-1 derives a descriptor-set scope from the descriptor state itself, which is what closes §5.5.8's one-binding-two-buffers hazard. But the encoder issued vkCmdBindDescriptorSets from inside BindStorageBuffer/BindUniformBuffer, so the arena resolved after EVERY bind. For the retained UI's one or two binds that is free. For a world renderer binding ten buffers it materialises up to ten scopes per draw — nine of them PARTIAL states no draw ever uses, each claiming a real descriptor-set pair out of a fixed-size pool and each paying a full round of vkUpdateDescriptorSets. Recording the state and resolving it once, where the draw needs it, yields exactly one scope per renderer, which is what the arena was designed to produce. It is legal because descriptor-set binding is independent of pipeline binding when the layouts are compatible, and acdream has ONE pipeline layout by design (§4.4) — the same property that lets a bucketed pass change pipeline for free. The pass still opens with all three sets bound, which is V6h's fix for VUID-vkCmdDraw-None-08600 and stays exactly as it was. Gates. Release build green. App tests 4,112 passed / 3 skipped, unchanged from commit 1. Strict GL offline pixel gate against commit 1: 3.73e-05 (21 differing pixels of 563,200), inside the documented 9-31 px control band — expected, since no GL file is touched. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero validation errors, zero warnings, and no [shutdown] diagnostic on either stream. The captured Vulkan frame is compared against commit 1's rather than merely eyeballed: 0 differing pixels of 921,600, maximum channel delta 0 — bit-identical across the merge, which is the strongest available evidence that moving the clear into the world pass changed nothing about what is drawn. What this does NOT do: draw a world. See the report for the enumerated remainder. No divergence-register row: no retail-facing behaviour changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fe8abacfc6 |
feat(render): Campaign V slice V6i-3 commit 1 — the mesh pipeline's upload bodies cross the seam
V6i-2 cut IMeshPipelineDevice at the measured surface and proved the mesh
pipeline could be CONSTRUCTED without naming a backend. It said plainly what it
did not claim: "the mesh pipeline does not RUN on Vulkan. Its upload bodies are
still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer transfers."
This moves them, and gives the interface its second implementation.
GlobalMeshBuffer takes GL?. The two backing stores were already IGpuBuffer
(V4b); what still needed a context was the vertex array and the attribute
pointers, which have no RHI verb because Vulkan bakes vertex input into the
pipeline. So a backend with none builds the stores and nothing else, publishes
0 for VAO/VBO/IBO, and publishes VertexStore/IndexStore — the same buffers,
named the way a pass encoder binds them. HasStores is the backend-neutral form
of the VAO != 0 readiness test the raw-GL draw paths make. Two bodies fork on
the context and nothing else does: InitBuffers skips the vertex array, and
CommitMigration skips the rebind — on the encoder arm the field swap IS the
atomic publication, because the next pass reads whatever the field then holds.
The store deletion likewise splits: GL keeps its immediate DeleteRetired,
because the arena's own flight gate has already proven no submitted frame can
reference the store, while the other arm has no second deferral to skip and
Dispose is its retirement-queued release.
ObjectMeshManager's RequireGl narrowed to the LEGACY per-mesh upload. Its three
call sites were one modern-path constructor argument and two bodies whose every
GL statement sits inside `if (!_useModernRendering)`. The constructor now hands
the arena the nullable context; the two bodies resolve one lazily inside the
legacy branch. That branch is unreachable in every shipping configuration —
missing bindless or draw-parameters throws at startup under the N.5 ship
amendment — so the accessor survives as the guard on dead code rather than as a
blocker, and it is deleted with that code.
VulkanMeshPipelineDevice is the second implementation, and it is four
properties and two no-ops. Two things about it are worth stating rather than
leaving to be inferred. HasBindless and HasOpenGL43 answer TRUE: their names are
GL-shaped because the seam was cut from a GL device, but what they gate is the
MODERN path — one shared arena, table texture indexing, multi-draw indirect —
which Vulkan supplies unconditionally and the capability gate rejects a device
for lacking, so answering false would disable the only path that exists.
HasPendingWork answers false because the GL device's queue exists to defer work
onto the thread holding the context, and Vulkan resource work is recorded into
the frame's command buffer or routed through the retirement queue.
WbMeshAdapter selects between them once, in the one place the mesh pipeline
still names a backend. The GL arm is unchanged, including the queue-drain
guarantee its construction rollback asserts.
So composition builds the mesh pipeline on BOTH arms, and NullWbMeshAdapter is
deleted — it existed for exactly the gap this closes, and the landblock spawn
ledger now registers against the real adapter. Streaming's publication into GPU
state stops being a no-op there: the Vulkan run below builds real render data,
including the [up-null] zero-vertex caching path.
Gates. Release build green. App tests 4,112 passed / 3 skipped, against a 4,109
baseline plus the three added here. Strict GL offline pixel gate against
|
||
|
|
c8d0f70bbe |
feat(render): Campaign V slice V6i-2 commit 2 — world texture creation crosses to IGpuTexture
Plan §5.5.11 recorded what V4t deliberately left behind: it moved the table
ENTRY of every world texture to the device and kept CREATION with the caches,
because "creating world textures through IGpuTexture is real remaining work and
it belongs with the Vulkan world arm, which is the first thing that cannot use a
GL handle at all." §5.5.12 item 1 handed it forward and named the missing piece
exactly — "an ITextureArray implementation over IGpuTexture, not a codec",
because V6b's BlockCompressionCodec and BlockCompressionMipChain already supply
the BC chains. This is that work.
IWorldTextureArray is the seam, and the slot is what crosses it. Before this
commit ObjectMeshManager read BindlessWrapHandle/BindlessClampHandle off the
concrete GL array and interned them into the device table itself. A 64-bit
ARB_bindless_texture handle has no Vulkan spelling, so the array now answers the
question the caller was really asking — ResolveSlot(wrapping) — and each arm gets
there its own way: ManagedGLTextureArray makes the same idempotent interning call
one level down, and RhiWorldTextureArray returns a pair it registered at
construction. ReleaseTextureSlots replaces the snapshot dictionary the manager
kept for the same reason, and still runs only once physical retirement completes.
Which implementation exists is decided ONCE, by the IWorldTextureArrayFactory
composition builds — plan §3.1's no-runtime-fork rule. Everything above the seam
(capacity policy, slot allocation, ref counting, layer retirement, empty-atlas
eviction, and the whole of ObjectMeshManager's atlas policy) is written once and
branches on nothing.
Three things the RHI array does differently, each because the backends genuinely
differ rather than by choice: BC mip chains are CPU-built through
BlockCompressionMipChain, since Vulkan cannot blit into a compressed image, while
RGBA8 uses the device's blit; filtering lives in an immutable sampler rather than
a texture parameter, so both address modes are registered up front exactly as the
GL array holds two resident handles; and RGB8/A8/Rgba32f are refused at creation
with the reason named. A8 is the interesting refusal — the GL array serves it by
swizzling R into A, and a Vulkan swizzle lives in the image VIEW, which the pinned
GpuTextureDescription does not describe. A silent substitution would render wrong
and look like a shader bug.
TerrainAtlas gains the second construction path V6i drafted and reverted. The
decode is factored out and shared, so both arms read the same DATs, in the same
order, with the same resize-to-max policy; only the upload forks.
ICompositeTextureArrayBackend gains its RHI arm, which is four small methods
because that seam was already a seam.
The Vulkan arm is EXERCISED, not merely present. That is the whole reason the
V6i draft was reverted rather than landed — "built then reverted because nothing
exercised it" — and it is the same failure §5.5.12 measured twice in the
descriptor layouts. So the composition host now builds the real terrain atlas
through IGpuDevice.CreateTexture on the arm with no GL context, and creates and
releases one shared array of each format family plus one composite array at
startup. Creation only; nothing draws them. Releasing them in the same statement
covers one thing a retained bundle would not — that both slot pairs come back and
the images route through the retirement queue.
Gates: Release build; App tests 4,104 / 3 skips; strict GL offline pixel gate vs
|
||
|
|
b8bcaa3ef2 |
feat(render): Campaign V slice V4t-1 — terrain crosses to GpuTextureSlot
V4t moves the world texture stack off the raw 64-bit ARB_bindless_texture handle and onto GpuTextureSlot. This first commit does terrain only, because terrain is the one branch of that stack whose producer and consumer are a single pair — TerrainAtlas and TerrainModernRenderer — so it can carry the new device seam on its own pixel gate before the mesh/composite/particle retype lands on top of it. Why the device's table can now be reached, when §5.2 said it could not. That paragraph's reason was the flush: GlGpuDevice drains its dirty table runs inside FlushBeforeDraw, which only an encoder-recorded draw reaches, so a raw-GL renderer would sample a stale table. §5.5.6 then closed the GL re-land of V4c/V4d, which means the world renderers stay raw GL through to V10 — so "wait for the encoder" stopped being a plan and became an indefinite block on V4t, which the Vulkan world arm cannot be written without. The resolution is the smallest one that keeps the seam honest: the drain is factored out as GlGpuDevice.FlushTextureTable, and a raw-GL renderer calls it and binds TextureTableGlName at binding 9 itself, immediately before its own draw — the same shape its retired private GlBindlessHandleTable had, against a table that is now the device's. Nothing else of the backend is exposed, and both members are deleted with the raw-GL world path. Residency ownership deliberately does NOT move. RegisterWorldTextureHandle interns an already-resident handle and owns only the table entry; the atlas still creates, makes resident and destroys its own textures. That is what separates it from RegisterTexture, which owns the residency it creates, and it is why this slice can retype the data model without also porting GL texture creation onto IGpuTexture. TerrainAtlas.GetBindlessHandles becomes GetTextureSlots(GlGpuDevice). Registration is idempotent by handle, so the per-draw call is two dictionary lookups — the cadence GetOrAdd already had. It is conditional on the handle having changed because SetAnisotropic makes both textures non-resident and re-acquires them: without that check a quality-preset change would strand a slot holding a non-resident handle, so the superseded entry is retired in the same step through the device's retirement queue. Ordering is unaffected. Terrain's two slots travel as loose uniforms (uTextureIndexA/B) and enter no sort and no bucket key, so a different slot NUMBER changes nothing about what is drawn or in what order — only which table index resolves to the same handle. Gates. GL offline pixel gate vs |
||
|
|
b16f820643 |
feat(render): Campaign V slice V6h — the Vulkan composition host
ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against
|
||
|
|
7faaaa347b |
feat(render): V6e — the sky's uniforms become a buffer and its texture a table slot
Campaign V slice V6e, last of three. Sky was the hardest of the four pairs
because it was the only one that still worked the way a 2004 shader works: a
dozen loose uniforms pushed one glUniform call at a time, and a texture bound to
unit 0 with a sampler object chosen per submesh. Vulkan GLSL has neither a
default uniform block nor a way to declare a bare sampler, so both had to move —
and the second one had a sting in it.
The uniforms go into a `SkyParams` std140 block at uniform binding 4, the new
pre-authorized constant in GpuBindingModel (1, 2 and 3 are SceneLighting, the
terrain clip block and terrain tiling; the contract test now proves the three
constants and that literal 2 do not collide). Three matrices are 192 bytes on
their own, so the 96-byte push-constant block was never in the running. The
block's member order IS its layout: std140 aligns a vec3 to 16 bytes while using
12, so each of the three lighting vectors is followed by the float that rides in
its pad word, which is why colours and per-surface scalars interleave rather
than grouping by meaning. SkyParamsLayoutTests asserts all twelve offsets and
the 256-byte size, because getting one member wrong would read the sun direction
as a colour with no compile error, no link error and no GL error to say so.
The texture is the interesting half. sky.frag now reads through the shared table
(ACDREAM_SAMPLE_2D), and a bindless handle BAKES its sampler — so the
per-submesh Repeat-versus-ClampToEdge choice, which used to be a glBindSampler
on unit 0, becomes which slot the submesh asks for. SkyRenderer interns one
handle per (texture, wrap) pair, exactly as ManagedGLTextureArray has done since
the world path went bindless, and exactly the shape Vulkan's table has, where an
entry is a combined image sampler. Same two SamplerCache objects, same wrap
behaviour, consulted once at interning instead of once per draw. A pleasant
consequence: the sky no longer touches texture unit 0, so the load-bearing
`BindSampler(0, 0)` restore at the end of the pass — there because the binding
was global state that would otherwise force ClampToEdge on the next renderer —
has nothing left to undo and is gone.
Gates. Release build clean; App tests 4,072 passed / 3 skipped (4,057 baseline,
plus the sentinel guard from the previous commit and fourteen sky-layout
assertions). Offline pixel gate against
|
||
|
|
543bc79f8a |
Revert "feat(render): Campaign V slice V4c - move the world draw path onto the RHI"
This reverts commit
|
||
|
|
ad61f250fb |
Revert "feat(render): Campaign V slice V4d-2 - move terrain onto the RHI"
This reverts commit
|
||
|
|
b064668b63 |
feat(render): Campaign V slice V4d-2 - move terrain onto the RHI
TerrainModernRenderer records through IGpuPassEncoder instead of calling GL directly. V4d-1 already converged its two matrix uniforms; this is the plumbing. What moved. The per-frame indirect command array became an IGpuFrame.AllocateRing slice, which retires the three-deep per-frame-slot indirect buffer pool outright. That pool existed so a second terrain draw within one frame - a retail outside view can issue several - could not overwrite an earlier draw's still-pending commands; the frame ring gives that structurally, because every allocation within a frame is distinct memory that lives until the frame retires. DynamicIndirectBufferCount now reports 0, which is the truth rather than a silent change. The vertex and index arena became an IGpuBuffer pair. AddLandblock's two BufferSubData calls are Upload, and EnsureCapacity's grow-and-copy is IGpuBuffer.CopyTo, still device-side so resident landblock meshes never round-trip through system memory. The global VAO is gone: the pipeline owns one shaped by the vertex layout, and the encoder re-issues attribute pointers on every BindVertexBuffer. Locations 2-5 use GpuVertexFormat.UByte4UInt, added at |