diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index ee1be0a3..099f8f1a 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -1120,7 +1120,12 @@ and physics cross-cell membership one canonical owner:** registration/move transaction and retains it both as collision rows (`GetOwnerCells`) and per-cell `RetailPartEntry` render rows (`GetRetailPartEntriesInCell`) — one flood drives both, never two -independent ones. `WalkProductionWorldData` no longer reconstructs +independent ones. The equipped-child controller publishes every DAT-resolved +effective Setup part (after `AnimPartChanged`) through the same strict +prepared-collision publisher used by ordinary live hydration before it calls +`AttachChild`; children still inherit the parent's CELLARRAY and acquire only +render part-entry rows, never an independent collision registration or flood. +`WalkProductionWorldData` no longer reconstructs membership from scene records, prepared visual AABBs, or per-record aggregate shadow-owner cells: `GetCellStatics`/`GetCellDynamics`/ `GetOutdoorStatics`/`GetOutdoorDynamics` are a borrowed, on-demand view over diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index d62594a2..89276ac4 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -726,11 +726,15 @@ this ledger, unchanged five reviewed blobs, campaign73de7403c. Fresh integrated Release0W0E/314GPU PASS; native r2 exits0/gracefully with zero validation hazards.477-integrated-validation-r2.md preserves PNG FAIL/limited PASS; original AMD causation/ordinary soak and G4 remain open. -#476 separately dispatched to James at6347e0f82; #480 returned eight files, -agent92/92 tests, pending lead verification/reviews. No new client run. Lead verified -#480's retail/paired-binary child publication and contracted the separate -shared prepared-cache fix in480-equipped-render-publication-contract.md; -Rawls implements it in fresh s5-480-openai-impl at9556c1748. +#480 lead verification complete: two actual consumer-row mutations FAIL as +intended, exact restoration, Release0W0E/92PASS. Wegener retail/behavior1 and +James production/lifetime2 both PASS, no findings; one prior test-only F1. +Reviewed scratch e58f3cc2c integrates with this ledger; architecture preserves +#477.480-lead-verification.md records proof; connected hand-item PNG owed. +#476 returned twelve files; lead independent two mutations, exact restoration, +Release0W0E/175PASS.476-lead-verification.md records proof; Rawls review1 in +flight, production/lifetime2 follows. Capture fix not integrated yet. No new +client run; both clients absent and ACE9000 PID13340 up at latest preflight. Rynthid Recall6150→3032001C corroborates owner generation4; historical route labels are not location evidence (479-rynthid-location-evidence.md). #479 transform cause remains unproven. No change to G4 FAIL or the open obligations. diff --git a/docs/research/2026-09-01-overhaul/476-lead-verification.md b/docs/research/2026-09-01-overhaul/476-lead-verification.md new file mode 100644 index 00000000..34320e4a --- /dev/null +++ b/docs/research/2026-09-01-overhaul/476-lead-verification.md @@ -0,0 +1,67 @@ +# #476 — completed-frame capture, lead verification + +2026-09-05. Frozen twelve-file scratch s5-476-openai-impl at +6347e0f8269e201340b0c7a6b826ad4cfc21f43e, including new Vulkan validity tests. +Pre-integration proof only. Root read every production/test diff and actual +composition, failure paths, capture controller and Vulkan callers. + +Same optional screenshot adapter moves from the still-open private renderer to +the outer orchestrator: drawing, measurement End, exactly one GPU End, capture, +CPU diagnostics, then post diagnostics. ScreenshotCaptured is replaced with the +actual post-close result; other immutable private/portal facts remain those of +the drawn frame. Begin failure/zero area do not close; rendering failure retains +recovery-before-close and aggregate ordering. End failure skips capture; later +capture/diagnostic failure cannot recover or close an already closed frame. + +The actual Vulkan owner clears capture validity at Begin and allocation/resize, +records copy presence after CmdCopyImageToBuffer, and advertises it only after +checked QueueSubmit2. Readback rejects missing submitted copy before the existing +DeviceWaitIdle/read. Off remains opt-in with no new waits, copies or allocations. +The unchanged bring-up using(frame) closes before its capture at +VulkanBringUpHost.cs:180-207; the main fix adds no warmup or discarded first PNG. + +## Independent lead gates + +Both clients absent before every command; no graphical launch. Lead Release +build 0 warnings/0 errors (final3.40s), combined focused/affected lane175/175, +zero skips. Tests include real orchestrator + private renderer + real screenshot +controller, fake GPU pixels published only by End, startup and actual resize +RGBA/orientation, pending requests, zero-area/resume and failure boundaries. +Actual production validity state is exercised with source guards binding its +real Begin/copy/submit/resize/read callers; these tests do NOT execute native GPU +commands. Root also inspected implementer's literal App7096/7096 TRX and hash, +not a root rerun or complete-solution test claim. + +Two root actual-production mutations, separately restored with exact raw hashes: + +| Mutation | Intended failure | Root TRX under artifacts/476-root | +|---|---|---| +| Move capture before End | first expected RGBA(1,2,3,4), actual(238,238,238,238) | m1/root-476-m1.trx | +| Mark RecordCopy submitted-valid immediately | Assert.False sees True before submission | m2/root-476-m2.trx | + +TRX SHA256 respectively: +641B33AE025E448C364C59ADFFD1EB1548C74C8B5D508B688BD996BEE934F735; +17D37A3764A002F10AD7D11662FE3DE8DB6EF2CEA3618D1B2D1ACF31FB9AB607. +Final artifacts/476-root/final/root-476-final175.trx: +664487A18F44115D5FE7D1CD4417FFDD1479B09E6C4ECA5351187DADC3C0711B. +Implementer artifacts/476-round0/app-hermetic-final/476-app-hermetic-final.trx: +4DE8F9A1C77BA0BB93F7C018D358FCD6928A362962AD005D868809C5867C7FE7. +Its filter excludes InstalledDat, Linux, Manual, Timing, Live, PreparedPackage. +Initial fresh-scratch no-restore solution build failed42 NETSDK1004 missing +assets; subsequent restore-enabled full57-project Release build passed0W0E. + +Root verified all twelve returned raw hashes and git diff --check. Key hashes: + +- Orchestrator:9C15760F12D8732347988017E5708CB4DD2724B6858578F2F5C2A8891912201C +- Device:493E446C42B60ABBE473460F08C97BC3A152F9EA222F2ED1C8071E8ABBA6D159 +- Resources:2844C34BB39FE648C9CCE1C421ED3BCEA330FB00C2182AA45899F8BC8927AD55 +- Validity tests:FB7E47486BAB58FB3E3DDF5D0B66163057C1E13A0FF468C8CF970D4CC6D1485E + +Named retail + paired-executable screenshot-boundary proof remains in +476-capture-lifetime-diagnosis.md. Vulkan retained acquired-image copying is a +modern adaptation; no new AC algorithm or intentional deviation is introduced. +Rawls independent behavior/retail review1 in flight; sequential production/ +lifetime review2 follows. No integration or graphical freshness PASS yet. +After review, merge capture-only portions without replacing #477's synchronization +changes, rebuild, then preserve the first PNG and two real distinct-size captures. +All visual results remain provisional until the owner looks; G4 still FAIL. diff --git a/docs/research/2026-09-01-overhaul/480-equipped-render-publication-contract.md b/docs/research/2026-09-01-overhaul/480-equipped-render-publication-contract.md index fedf4d55..05c57a45 100644 --- a/docs/research/2026-09-01-overhaul/480-equipped-render-publication-contract.md +++ b/docs/research/2026-09-01-overhaul/480-equipped-render-publication-contract.md @@ -99,3 +99,38 @@ updates the ledger. Coordinate graphics only after #477 validation is clean: verify equipped hand objects in a connected client, preserve PNG paths and label visual PASS provisional until owner inspection. This chunk cannot close the overall failed owner gate or authorize a main merge. + +## Implementation outcome (OpenAI round 0, 2026-09-05) + +Implemented in the dedicated uncommitted scratch for independent review. +`EquippedChildRenderController` now requires a GfxObj publication delegate and +invokes it for each non-null effective part while building availability, +before projection materialization or `AttachChild`. `LivePresentationComposition` +binds that dependency to `content.CollisionAssets.CacheGfxObj`, the same +composition-owned strict publisher ordinary live hydration receives. + +The focused controller witness drives the real Position-null `OnSpawn` route +from a cold `PhysicsDataCache` through a recording prepared source, a registered +parent CELLARRAY, `ShadowObjectRegistry` child rows, the live render journal, +`ArchRenderScene`, and `WalkProductionWorldData`. It covers a visual-only part, +an effective `AnimPartChanged` replacement, an absent-DAT part, repeat appearance +without duplicate rows or prepared rereads, strict missing-prepared failure +before projection/readiness, and real withdrawal preserving the parent's rows. +An IL ownership test retains the production composition binding. No deviation +row is created and no graphical acceptance is claimed here. + +## Fix round 1 outcome (OpenAI, 2026-09-05) + +The cold effective-part witness now reaches the actual inherited CELLARRAY +part rows and `WalkProductionWorldData` consumer before checking prepared-read +and cache diagnostics; every original assertion remains. Removing the real +publication call fails the row count with expected 2 / actual 0. Publishing +under original Setup ids instead of effective replacement ids fails the same +consumer with expected 2 / actual 1. Both production mutations were restored +to `EquippedChildRenderController.cs` SHA-256 +`5452088FB8980A5571B9007E73BE59A2FA9624659B1C146C9758D90680BCAD5E`. + +The final Release solution build completed with zero warnings and errors. The +same six-class affected lane passed 92/92 with no skips. Evidence is under +`artifacts/480-fix1-openai/`; no production source changed in this round and +no graphical result is claimed. diff --git a/docs/research/2026-09-01-overhaul/480-lead-verification.md b/docs/research/2026-09-01-overhaul/480-lead-verification.md new file mode 100644 index 00000000..a56e571f --- /dev/null +++ b/docs/research/2026-09-01-overhaul/480-lead-verification.md @@ -0,0 +1,76 @@ +# #480 — equipped-part publication, lead verification + +2026-09-05. Eight-file candidate at base9556c17480d042e2353de3dfddc70fb51a88c60e, +reviewed and committed by the lead as e58f3cc2c11cd4b5ee7c1da6725f45d5d1623168. +One test-only F1 corrected the order of consumer versus diagnostic assertions; +it did not change the production repair. No graphical item acceptance yet. + +Lead read all eight diffs and the actual controller/composition, strict +publisher, cache, inherited membership, Arch scene/walk and withdrawal paths. +Effective AnimPartChanged IDs are finalized before every DAT-present part +passes through content.CollisionAssets.CacheGfxObj, before materialization and +AttachChild. Missing DAT stays unavailable; missing prepared content fails +before new projection/readiness/membership. Partial immutable cache population +before a later strict failure is not a transactional rollback guarantee. +Children still inherit parent CELLARRAY rows, with no independent collision +registration/flood. Pose, authority, detach and attachment policy are unchanged; +the existing ignored AttachChild Boolean is outside this bounded repair. + +The real OnSpawn witness uses a cold strict prepared cache, real publication, +real child rows, live journal/Arch scene and WalkProductionWorldData. It checks +effective replacement identities, visual-only geometry, repeated appearance +without rereads/duplicate rows, strict failure and unparent withdrawal while +preserving parent rows. Fixture no-op delegates are not production fallbacks. + +## Independent execution and evidence + +Both AcDream.App and acclient absent before every build/test; no graphical +launch. Lead Release build after restoration: 0 warnings/0 errors (3.47s). +Final six-class affected lane: 92 passed, zero failures/skips. Classes: +EquippedChildProjectionWithdrawalTests, LivePresentationCompositionTests, +LiveEntityNetworkOnPositionCollapseMatrixTests, +LiveEntityNetworkRemoteTeleportPresentationTests, LiveCollisionAssetPublisherTests, +WalkProductionWorldDataTests. Command: dotnet test +tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release --no-build +--no-restore --nologo, FullyQualifiedName OR filter for those six classes. + +Two independent actual-production mutations, each restored, failed the intended +consumer assertion rather than compilation, setup or prepared-read diagnostics: + +| Mutation | Actual inherited child rows | Root TRX under artifacts/480-root | +|---|---|---| +| Remove _publishGfxObj call | expected2, actual0 | m1/root-480-m1.trx | +| Publish original Setup IDs instead of effective IDs | expected2, actual1 | m2/root-480-m2.trx | + +TRX SHA256 respectively: +CB885176B17C72B5909BFD15626FC6AE4AB343A5CC57F455A2F9C909E252BB8B; +1AF21E2F1BA8723E14097A9B3BD5AA7AD5B6BA0C6C3F012B0BAADD10E8CDF70F. +Final artifacts/480-root/final/root-480-final92.trx: +C9EA5D0D473A1DFC63860F46E0179263DA4C1373E51338056E96401F1E8D8C06. +Lead read actual XML ErrorInfo/counters. These are managed tests, not GPU/PNG proof. + +Restoration was checked by raw SHA256. apply_patch restored the code; its +mutation of one original CRLF call line required a newline-only mechanical +restoration, guarded by the expected full-file hash in memory before writing. +The final rebuild/test above followed that exact restoration. Final hashes: + +- Controller:5452088FB8980A5571B9007E73BE59A2FA9624659B1C146C9758D90680BCAD5E +- Composition:F501EEA2928B0503F10178BDBC37E1CD5F8993DE06230DD258913FD382831DF0 +- Consumer test:32824B6151B01E4A7FDDF41EC7BD2C7E0A2AFEC34CF1F622C466C1A818B56E4B + +## Retail and review boundary + +Lead independently verified named CPhysicsObj::add_shadows_to_cells00514AE0 +and CPartArray::AddPartsShadow00517E40 against the paired executable SHA256 +006FFEADC5D679C871497112A5BD1F87714D0E273E2166BAE5052DDE369297B1. +Range hashes/call bytes are in478-480-readonly-diagnosis.md. Retail recursively +adds every non-null child part to the same CELLARRAY; the shared prepared-cache +publisher is this client's modern implementation, not a claim about retail APIs. + +Wegener independent retail/behavior lens1 PASS; James production/lifetime +lens2 PASS. Both read actual production/consumer paths, mutation TRX and hashes; +neither reran tests or claimed graphical acceptance. No findings in either +review, one prior evidence-only F1. Lead integrates the reviewed source/test/ +contract blobs; architecture merges only the equipped-child paragraph and keeps +the already-landed #477 GPU paragraph. No new intentional deviation. +Fresh integrated checks and a connected hand-item PNG remain next. G4 FAIL. diff --git a/docs/research/2026-09-01-overhaul/481-entry-profile-recipe.md b/docs/research/2026-09-01-overhaul/481-entry-profile-recipe.md index 56bc2a0b..f895a099 100644 --- a/docs/research/2026-09-01-overhaul/481-entry-profile-recipe.md +++ b/docs/research/2026-09-01-overhaul/481-entry-profile-recipe.md @@ -7,12 +7,12 @@ timestamp pairing and PerformanceTools summarizer. No instrumentation change. After pending repairs integrate, use the same green Release, session-v10, High/near4/far25/MSAA4/aniso16/A2C1/maxCompletions4 and1600x900 profile. Before launch both clients absent/ACE9000 up; pause agent builds/tests. One -run-selfgate.ps1 invocation with -Label481-entry-profile (with the normal -space after the switch), -Route tools/overhaul-selfgate/route-481-entry-profile.txt, --Session logs/s5-c5-session-v10.json, -TimeoutSeconds240. No client yet. +run-selfgate.ps1 invocation with -Label 481-entry-profile, +-Route tools/overhaul-selfgate/route-481-entry-profile.txt, +-Session logs/s5-c5-session-v10.json, -TimeoutSeconds 240. No client yet. Fresh process environment: ACDREAM_DEVTOOLS=0; clear VK_INSTANCE_LAYERS, -VK_LAYER_ENABLES,VK_LAYER_VALIDATE_SYNC,ACDREAM_WB_DIAG; UI_PROBE_DUMP=0; +VK_LAYER_ENABLES,VK_LAYER_VALIDATE_SYNC,ACDREAM_WB_DIAG; ACDREAM_UI_PROBE_DUMP=0; ACDREAM_FRAME_PROF=1, ACDREAM_FRAME_HISTORY=. Create its artifact directory without overwriting any earlier run. No validation, screenshots, pack switches, resize or movement during samples. diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md index 49749293..c695bb7f 100644 --- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md +++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md @@ -4745,3 +4745,23 @@ FirstPNGstillFAIL and next retains portal caption unlike its checkpoint; #477 ordinary soak/AMD attribution and full G4 remain open. #476/480 builds released after exit. #480 F1 test-only consumer-assert ordering contracted at17212c810; no production defect found in returned patch. + +## 52. Hand-item repair accepted; capture independently verified + +#480 F1 closed with real consumer assertions: root removed publication and +restored original IDs separately, observed expected2/actual0 and expected2/ +actual1 inherited rows, restored exact hashes and passed Release0W0E/92tests. +Wegener retail/behavior1 PASS and James production/lifetime2 PASS, no findings. +Root committed reviewed scratch e58f3cc2c11cd4b5ee7c1da6725f45d5d1623168 and +integrates with this ledger, preserving the #477 architecture paragraph. +480-lead-verification.md has commands/TRX/hashes and retail boundaries. +No new intentional deviation; connected hands PNG remains owed. + +#476 twelve-file return independently read/tested: early-capture mutation +fails exact new-frame pixels; premature-validity mutation fails before submit. +Exact restoration, Release0W0E and175/175 root tests; implementer literal +App7096 TRX inspected, not root rerun.476-lead-verification.md records proof. +Rawls behavior/retail1 in flight; sequential production/lifetime2 follows. +Capture not integrated/graphically accepted. Root will preserve #477's separate +Vulkan changes during integration. Both clients absent; ACE9000 PID13340 up. +No new graphical run. All §49 obligations and G4 FAIL remain current. diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index ea1ad600..f4b95ae9 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -624,7 +624,8 @@ internal sealed class LivePresentationCompositionPhase projectionVersion, d.PlayerIdentity.ServerGuid), d.PhysicsEngine.ShadowObjects, - d.PhysicsDataCache), + d.PhysicsDataCache, + content.CollisionAssets.CacheGfxObj), static value => value.Dispose()); Fault(LivePresentationCompositionPoint.CorePresentationCreated); diff --git a/src/AcDream.App/Rendering/EquippedChildRenderController.cs b/src/AcDream.App/Rendering/EquippedChildRenderController.cs index 546295ea..e6f64e32 100644 --- a/src/AcDream.App/Rendering/EquippedChildRenderController.cs +++ b/src/AcDream.App/Rendering/EquippedChildRenderController.cs @@ -56,6 +56,16 @@ public sealed class EquippedChildRenderController : IDisposable /// private readonly PhysicsDataCache _physicsData; + /// + /// #480: the SAME strict prepared-collision publisher ordinary live + /// hydration uses. An attached child resolves its own effective visual + /// part ids (including AnimPartChanged replacements), so those ids must + /// enter the shared prepared cache before the child publishes retail + /// part-entry membership. This is a required composition dependency, + /// never an optional graph-only fallback. + /// + private readonly Action _publishGfxObj; + private ParentAttachmentState Relations => _liveEntities.ParentAttachments; /// Raised after the attached projection is fully registered. @@ -122,7 +132,8 @@ public sealed class EquippedChildRenderController : IDisposable Func withdrawProjection, ShadowObjectRegistry shadows, - PhysicsDataCache physicsData) + PhysicsDataCache physicsData, + Action publishGfxObj) { _dats = dats ?? throw new ArgumentNullException(nameof(dats)); _datLock = datLock ?? throw new ArgumentNullException(nameof(datLock)); @@ -134,6 +145,8 @@ public sealed class EquippedChildRenderController : IDisposable ?? throw new ArgumentNullException(nameof(withdrawProjection)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); _physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData)); + _publishGfxObj = publishGfxObj + ?? throw new ArgumentNullException(nameof(publishGfxObj)); _parentOfAttached = static child => child.ParentRecord.ProjectionKey; _tickAttached = TickChild; _reconcileAttached = ReconcileChild; @@ -1178,7 +1191,18 @@ public sealed class EquippedChildRenderController : IDisposable { var available = new bool[template.Count]; for (int i = 0; i < template.Count; i++) - available[i] = _dats.Get(template[i].GfxObjId) is not null; + { + uint gfxObjId = template[i].GfxObjId; + GfxObj? gfxObj = _dats.Get(gfxObjId); + if (gfxObj is null) + continue; + + // The effective identity is already final here. Publish its + // strict prepared payload before any attached projection or + // retail part-entry membership can become observable. + _publishGfxObj(gfxObjId, gfxObj); + available[i] = true; + } return available; } diff --git a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs index 3eeedfe8..924d8125 100644 --- a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs @@ -154,6 +154,20 @@ public sealed class LivePresentationCompositionTests field => field.FieldType == typeof(GameWindow)); } + [Fact] + public void EquippedChildConstructionBindsTheCompositionCollisionPublisher() + { + IReadOnlyList calls = + CompiledCallGraph.ReadOwned(typeof(LivePresentationCompositionPhase)); + + Assert.Contains(calls, call => + call.Target.DeclaringType == typeof(EquippedChildRenderController) + && call.Target.IsConstructor); + Assert.Contains(calls, call => + call.Target.DeclaringType == typeof(LiveCollisionAssetPublisher) + && call.Target.Name == nameof(LiveCollisionAssetPublisher.CacheGfxObj)); + } + private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(static _ => { }, static _ => { })); diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs index 50aa86f4..f12d1439 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs @@ -1390,7 +1390,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests ExactProjectionWithdrawalDisposition.Superseded, null), Shadows, - new PhysicsDataCache()), + new PhysicsDataCache(), + static (_, _) => { }), new ProjectileController(Runtime), animatedEntities, new RemoteMovementObservationTracker(), diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.cs index 1b114f6e..211af7fb 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.cs @@ -655,7 +655,8 @@ public sealed class LiveEntityNetworkRemoteTeleportPresentationTests ExactProjectionWithdrawalDisposition.Superseded, null), Shadows, - new PhysicsDataCache()), + new PhysicsDataCache(), + static (_, _) => { }), new ProjectileController(Runtime), animatedEntities, new RemoteMovementObservationTracker(), diff --git a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs index a1ec1403..834139fa 100644 --- a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs @@ -1,11 +1,17 @@ using System.Collections; using System.Numerics; using System.Reflection; +using System.Collections.Immutable; +using AcDream.App.Physics; using AcDream.App.Rendering; +using AcDream.App.Rendering.Scene; +using AcDream.App.Rendering.Scene.Arch; +using AcDream.App.Rendering.Walk; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Content; +using AcDream.Content.Pak; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; @@ -1654,6 +1660,320 @@ public sealed class EquippedChildProjectionWithdrawalTests } } + [Fact] + public void OnSpawn_ColdEffectivePartsPublishPreparedMembershipIntoWalkAndWithdrawCleanly() + { + const uint basePart = 0x0100E001u; + const uint originalPart = 0x0100E002u; + const uint replacementPart = 0x0100E003u; + const uint absentDatPart = 0x0100E004u; + const uint parentGuid = 0x70000E00u; + const uint childGuid = 0x70000E01u; + + Setup setup = AttachedSetup(basePart, originalPart, absentDatPart); + var gfxObjs = new Dictionary + { + [basePart] = DrawableGfx(0x0800E001u), + [originalPart] = DrawableGfx(0x0800E002u), + [replacementPart] = DrawableGfx(0x0800E003u), + }; + var prepared = new RecordingPreparedCollisionSource( + basePart, + originalPart, + replacementPart); + ControllerFixture? fixture = null; + fixture = new ControllerFixture( + (record, positionVersion, projectionVersion) => + { + bool completed = fixture!.Live.WithdrawLiveEntityProjection( + record, + positionVersion, + projectionVersion); + return new ExactProjectionWithdrawalOutcome( + completed + ? ExactProjectionWithdrawalDisposition.Completed + : ExactProjectionWithdrawalDisposition.Superseded, + Failure: null); + }, + setup, + gfxObjs, + prepared); + using (fixture) + using (var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1))) + { + Assert.Equal(0, fixture.PhysicsData.FlatGfxObjCount); + LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: 1); + fixture.RegisterParentRetailMembership(parent); + Assert.True(fixture.Shadows.TryGetRetailCellArray( + parent.WorldEntity!.Id, + out IReadOnlyList parentCells)); + Assert.NotEmpty(parentCells); + + var journal = new RenderProjectionJournal( + RenderSceneGeneration.FromRaw(1)); + var projections = new LiveRenderProjectionJournal( + fixture.Live, + journal, + new GpuWorldRenderTraversalOrderSource(fixture.Spatial)); + Assert.True(projections.OnEntityReady( + LiveEntityReadyCandidate.Capture(parent))); + + int readyCount = 0; + fixture.Controller.EntityReady += candidate => + { + readyCount++; + Assert.True(projections.OnEntityReady(candidate)); + }; + fixture.Controller.ProjectionPoseReady += + projections.OnProjectionPoseReady; + fixture.Controller.ProjectionRemoved += + projections.OnProjectionRemoved; + fixture.Poses.Publish( + parent.WorldEntity, + Array.Empty()); + + WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData( + childGuid, + generation: 1) with + { + Position = null, + ParentGuid = parentGuid, + ParentLocation = 0, + PlacementId = 0, + AnimPartChanges = + [ + new CreateObject.AnimPartChange(1, replacementPart), + ], + }; + childSpawn = childSpawn with + { + Physics = childSpawn.Physics!.Value with + { + Position = null, + Parent = new PhysicsAttachment(parentGuid, 0u), + AnimationFrame = 0, + }, + }; + fixture.Live.RegisterLiveEntity(childSpawn); + + fixture.Controller.OnSpawn(childSpawn); + + Assert.Equal(1, readyCount); + Assert.True(fixture.Live.TryGetRecord( + childGuid, + out LiveEntityRecord child)); + Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind); + Assert.NotNull(child.WorldEntity); + Assert.Equal( + [basePart, replacementPart, absentDatPart], + child.WorldEntity.MeshRefs.Select(static part => part.GfxObjId)); + + uint childLocalId = child.WorldEntity.Id; + Assert.True(fixture.Shadows.TryGetRetailCellArray( + childLocalId, + out IReadOnlyList childCells)); + Assert.Equal(parentCells, childCells); + Assert.Empty(fixture.Shadows.GetOwnerCells(childLocalId)); + foreach (uint cellId in childCells) + { + RetailPartEntry[] childRows = fixture.Shadows + .GetRetailPartEntriesInCell(cellId) + .Where(row => row.EntityId == childLocalId) + .ToArray(); + Assert.Equal(2, childRows.Length); + Assert.Equal((0, basePart), + (childRows[0].PartIndex, childRows[0].GfxObjId)); + Assert.Equal((1, replacementPart), + (childRows[1].PartIndex, childRows[1].GfxObjId)); + } + + journal.DrainTo(scene); + var walk = new WalkProductionWorldData( + new WalkBuildingRegistry(), + fixture.Shadows); + walk.BeginFrame( + scene.OpenQuery(), + ControllerFixture.Landblock, + renderCenterLbX: 1, + renderCenterLbY: 1); + foreach (uint cellId in childCells) + { + RenderProjectionRecord projected = Assert.Single( + walk.GetCellDynamics(cellId).Records, + record => record.Source.LocalEntityId == childLocalId); + Assert.Equal( + RenderProjectionClass.EquippedChild, + projected.ProjectionClass); + } + Assert.Equal(0, walk.UnregisteredRenderMembershipCount); + + Assert.Equal([basePart, replacementPart], prepared.GfxReads); + Assert.NotNull(fixture.PhysicsData.GetFlatGfxObj(basePart)); + Assert.NotNull(fixture.PhysicsData.GetFlatGfxObj(replacementPart)); + Assert.Null(fixture.PhysicsData.GetFlatGfxObj(originalPart)); + Assert.Null(fixture.PhysicsData.GetFlatGfxObj(absentDatPart)); + + Assert.True(fixture.Live.TryApplyObjDesc( + new ObjDescEvent.Parsed( + childGuid, + new CreateObject.ModelData( + BasePaletteId: null, + Array.Empty(), + Array.Empty(), + [new CreateObject.AnimPartChange(1, replacementPart)]), + InstanceSequence: 1, + ObjDescSequence: 1), + out _)); + Assert.True(fixture.Controller.TryApplyAttachedAppearance( + child, + child.ObjDescAuthorityVersion)); + Assert.Equal([basePart, replacementPart], prepared.GfxReads); + foreach (uint cellId in childCells) + { + RetailPartEntry[] childRows = fixture.Shadows + .GetRetailPartEntriesInCell(cellId) + .Where(row => row.EntityId == childLocalId) + .ToArray(); + Assert.Equal(2, childRows.Length); + Assert.Equal([basePart, replacementPart], + childRows.Select(static row => row.GfxObjId)); + } + + Assert.Equal( + ChildUnparentDisposition.Completed, + fixture.Controller.OnChildBecameUnparented(childGuid)); + Assert.False(fixture.Shadows.TryGetRetailCellArray( + childLocalId, + out _)); + foreach (uint cellId in parentCells) + { + IReadOnlyList rows = + fixture.Shadows.GetRetailPartEntriesInCell(cellId); + Assert.DoesNotContain(rows, row => row.EntityId == childLocalId); + Assert.Contains(rows, row => + row.EntityId == parent.WorldEntity.Id); + } + } + } + + [Fact] + public void OnSpawn_MissingPreparedEffectivePartFailsBeforeProjectionOrMembership() + { + const uint part = 0x0100E010u; + const uint parentGuid = 0x70000E10u; + const uint childGuid = 0x70000E11u; + Setup setup = AttachedSetup(part); + var gfxObjs = new Dictionary + { + [part] = DrawableGfx(0x0800E010u), + }; + var prepared = new RecordingPreparedCollisionSource(part); + prepared.MissingIds.Add(part); + using var fixture = new ControllerFixture( + static (_, _, _) => new ExactProjectionWithdrawalOutcome( + ExactProjectionWithdrawalDisposition.Superseded, + Failure: null), + setup, + gfxObjs, + prepared); + LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: 1); + fixture.RegisterParentRetailMembership(parent); + fixture.Poses.Publish(parent.WorldEntity!, Array.Empty()); + int readyCount = 0; + fixture.Controller.EntityReady += _ => readyCount++; + + WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData( + childGuid, + generation: 1) with + { + Position = null, + ParentGuid = parentGuid, + ParentLocation = 0, + PlacementId = 0, + }; + childSpawn = childSpawn with + { + Physics = childSpawn.Physics!.Value with + { + Position = null, + Parent = new PhysicsAttachment(parentGuid, 0u), + AnimationFrame = 0, + }, + }; + fixture.Live.RegisterLiveEntity(childSpawn); + + InvalidDataException error = Assert.Throws( + () => fixture.Controller.OnSpawn(childSpawn)); + + Assert.Contains("Missing", error.Message, StringComparison.Ordinal); + Assert.Equal([part], prepared.GfxReads); + Assert.Equal(0, readyCount); + Assert.False(fixture.Live.TryGetRecord(childGuid, out _)); + Assert.Empty(fixture.Controller.AttachedEntityIds); + Assert.True(fixture.Live.TryGetCanonical( + childGuid, + out RuntimeEntityRecord childCanonical)); + Assert.NotNull(childCanonical.LocalEntityId); + Assert.False(fixture.Shadows.TryGetRetailCellArray( + childCanonical.LocalEntityId!.Value, + out _)); + Assert.Equal(0, fixture.PhysicsData.GfxObjCount); + Assert.Equal(0, fixture.PhysicsData.FlatGfxObjCount); + } + + private static Setup AttachedSetup(params uint[] parts) + { + var setup = new Setup + { + HoldingLocations = + { + [(ParentLocation)0] = new LocationType + { + PartId = -1, + Frame = new Frame { Orientation = Quaternion.Identity }, + }, + }, + }; + for (int i = 0; i < parts.Length; i++) + setup.Parts.Add(parts[i]); + return setup; + } + + private static GfxObj DrawableGfx(uint surfaceId) => new() + { + Surfaces = { surfaceId }, + VertexArray = new VertexArray + { + Vertices = + { + [0] = new SWVertex + { + Origin = Vector3.Zero, + Normal = Vector3.UnitZ, + }, + [1] = new SWVertex + { + Origin = Vector3.UnitX, + Normal = Vector3.UnitZ, + }, + [2] = new SWVertex + { + Origin = Vector3.UnitY, + Normal = Vector3.UnitZ, + }, + }, + }, + Polygons = + { + [0] = new Polygon + { + PosSurface = 0, + NegSurface = -1, + VertexIds = { 0, 1, 2 }, + }, + }, + }; + private static LiveEntityRecord ChildRecord() => LiveEntityTestFixture.CreateExactProjectionRecord( new WorldSession.EntitySpawn( @@ -1672,9 +1992,83 @@ public sealed class EquippedChildProjectionWithdrawalTests MotionTableId: null, InstanceSequence: 1)); + private sealed class RecordingPreparedCollisionSource( + params uint[] availableIds) : IPreparedCollisionSource + { + private static readonly FlatPhysicsBsp EmptyPhysics = new( + -1, + ImmutableArray.Empty, + ImmutableArray.Empty, + FlatPolygonTable.Empty); + private readonly HashSet _availableIds = [.. availableIds]; + + internal List GfxReads { get; } = []; + internal HashSet MissingIds { get; } = []; + + public PreparedAssetPresence ProbeCollision( + PakAssetType type, + uint sourceFileId) => + type == PakAssetType.GfxObjCollision + && _availableIds.Contains(sourceFileId) + && !MissingIds.Contains(sourceFileId) + ? PreparedAssetPresence.Available + : PreparedAssetPresence.Missing; + + public PreparedCollisionReadResult + ReadGfxObjCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) + { + GfxReads.Add(sourceFileId); + if (!_availableIds.Contains(sourceFileId) + || MissingIds.Contains(sourceFileId)) + { + return PreparedCollisionReadResult + .Missing; + } + + var bounds = new FlatGfxObjVisualBounds( + new Vector3(-0.5f), + new Vector3(0.5f), + Vector3.Zero, + 0.8660254f, + new Vector3(0.5f)); + return PreparedCollisionReadResult + .Loaded(new FlatGfxObjCollisionAsset( + EmptyPhysics, + BoundingSphere: null, + bounds)); + } + + public PreparedCollisionReadResult + ReadSetupCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) => + PreparedCollisionReadResult.Missing; + + public PreparedCollisionReadResult + ReadCellStructureCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) => + PreparedCollisionReadResult.Missing; + + public PreparedCollisionReadResult + ReadEnvCellTopology( + uint sourceFileId, + CancellationToken cancellationToken = default) => + PreparedCollisionReadResult.Missing; + + public PreparedCollisionSourceStats CollisionStats => default; + + public void Dispose() + { + } + } + private sealed class ControllerFixture : IDisposable { private const uint Cell = 0x01010001u; + internal const uint Landblock = 0x0101FFFFu; private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new(); private readonly Setup _setup; private readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController _firstEntry; @@ -1724,7 +2118,10 @@ public sealed class EquippedChildProjectionWithdrawalTests internal ControllerFixture( Func - withdraw) + withdraw, + Setup? setup = null, + IReadOnlyDictionary? gfxObjs = null, + IPreparedCollisionSource? preparedCollision = null) { Spatial.AddLandblock(new LoadedLandblock( (Cell & 0xFFFF0000u) | 0xFFFFu, @@ -1748,7 +2145,7 @@ public sealed class EquippedChildProjectionWithdrawalTests new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), _lifecycle, EntityObjects); - _setup = new Setup + _setup = setup ?? new Setup { HoldingLocations = { @@ -1761,6 +2158,14 @@ public sealed class EquippedChildProjectionWithdrawalTests }; IDatReaderWriter dat = DispatchProxy.Create(); ((NullDatProxy)(object)dat).Setup = _setup; + ((NullDatProxy)(object)dat).GfxObjs = gfxObjs; + Shadows = new ShadowObjectRegistry(); + PhysicsData = new PhysicsDataCache(); + Action publishGfxObj = preparedCollision is null + ? static (_, _) => { } + : new LiveCollisionAssetPublisher( + PhysicsData, + preparedCollision).CacheGfxObj; Controller = new EquippedChildRenderController( dat, new object(), @@ -1769,8 +2174,9 @@ public sealed class EquippedChildProjectionWithdrawalTests Poses, update => Live.TryApplyParent(update, out _), withdraw, - new ShadowObjectRegistry(), - new PhysicsDataCache()); + Shadows, + PhysicsData, + publishGfxObj); _lifecycle.Bind(new DelegateLiveEntityRuntimeComponentLifecycle( Controller.OnLogicalTeardown)); } @@ -1795,6 +2201,35 @@ public sealed class EquippedChildProjectionWithdrawalTests internal EntityEffectPoseRegistry Poses { get; } = new(); internal LiveEntityRuntime Live { get; } internal EquippedChildRenderController Controller { get; } + internal ShadowObjectRegistry Shadows { get; } + internal PhysicsDataCache PhysicsData { get; } + + internal void RegisterParentRetailMembership(LiveEntityRecord parent) + { + var sphere = new FlatCollisionSphere(Vector3.Zero, 1f); + IReadOnlyList parts = + [ + ShadowShape.Bsp( + 0x0100FFFFu, + Vector3.Zero, + Quaternion.Identity, + scale: 1f, + ShadowPartGeometry.Create(sphere, visualBounds: null)), + ]; + Shadows.RegisterMultiPart( + parent.WorldEntity!.Id, + parent.WorldEntity.Position, + parent.WorldEntity.Rotation, + parts, + state: 0u, + flags: EntityCollisionFlags.None, + worldOffsetX: 0f, + worldOffsetY: 0f, + landblockId: Landblock, + seedCellId: Cell, + isStatic: false, + partArray: parts); + } internal LiveEntityRecord Spawn( uint guid, @@ -2012,6 +2447,7 @@ public sealed class EquippedChildProjectionWithdrawalTests private class NullDatProxy : DispatchProxy { internal Setup? Setup { get; set; } + internal IReadOnlyDictionary? GfxObjs { get; set; } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { @@ -2020,6 +2456,15 @@ public sealed class EquippedChildProjectionWithdrawalTests { return Setup; } + if (targetMethod?.Name == "Get" + && targetMethod.ReturnType == typeof(GfxObj) + && args is [uint id]) + { + return GfxObjs is not null + && GfxObjs.TryGetValue(id, out GfxObj? gfxObj) + ? gfxObj + : null; + } if (targetMethod?.ReturnType == typeof(void)) return null; if (targetMethod?.ReturnType.IsValueType == true)