Commit f703b12
fix(AdornerLayer): remove unsafe _layoutDirty gate; add re-entrancy leases (#6)
The dirty-bit gate in OnLayoutUpdated (82dc1d5) was an unsafe
correctness hazard. The cached _layoutDirty flag cannot comprehensively
observe every coordinate-space change that requires re-walking adorners:
- ContentPresenter re-templating that rebuilds the intermediate
subtree between an adorned element E and the AdornerLayer parent
without firing layout on E itself
- Ancestor RenderTransform / scroll / clip changes
- Layer-parent changes
- ArrangeDirty propagation
- Stale-element cleanup when E exits the adorner-decorator subtree
For MotionCatalyst this manifested when opening a take inside the
AnalysisView: ContentPresenter swap rebuilt the intermediate visual
chain around the adorned elements (video viewports, data-box hosts),
their transform-to-ancestor changed, but neither their own LayoutUpdated
nor any Add/Remove/Update call armed _layoutDirty. OnLayoutUpdated hit
the gate and returned early; UpdateAdorner was skipped; adorners
rendered at stale positions. Symptom: take state transitioned but no
video frames and no data-boxes appeared.
The fix removes only the gate (line 676); the empty-AdornerLayer fast
path from dfe6bd4 stays intact (preserves the bigger 17x render
scheduling drop). The transform/size/clip change checks inside
UpdateElementAdorners still suppress redundant Adorner.InvalidateMeasure
calls, so the only steady-state cost is the per-pass ElementMap walk
plus TryTransformToAncestorAsMatrix calls. The element-level
LayoutUpdated subscription plumbing (SubscribeToElementLayout /
OnAdornedElementLayoutUpdated / _layoutDirty field / _subscribedElements
set) is retained but the field is now write-only; a future cleanup can
remove the unused machinery.
Companion safety: now that UpdateAdorner runs on every non-empty
LayoutUpdated fire, the pooled _removeList and _keysSnapshotBuffer
become re-entrancy hazards. If a custom Adorner's InvalidateMeasure
or InvalidateVisual override synchronously calls back into
AdornerLayer.Add/Remove, the nested UpdateAdorner would clobber the
outer call's lists mid-iteration. Wrap UpdateAdorner's body in
try/finally and lease both pools using the same pattern as
_zOrderValuesSnapshotBuffer in MeasureOverride/ArrangeOverride.
Multi-model consensus:
- 3x Opus agents in parallel ruled out other candidates (BooleanBoxes
in DataBindEngine, SimpleTransform fast path, _zOrderMap lease,
_keysSnapshotBuffer pool alone) - see investigation/2026-05-16
- gpt-5.5-pro (against stance, 7/10): "deleting line 676 is the safest
minimal correction"; do not bundle 9dce07e revert
- gemini-3.1-pro-preview (for stance, 9/10): "completely sufficient";
also recommended _removeList lease pattern (incorporated)
Validated end-to-end against MotionCatalyst's regression scenario via
binary-swap into BUILD/x64_Debug: take loads with all video viewports,
Pressure plate, Launch Monitor, and Torque/Force charts visible after
the ContentPresenter content swap.
Co-authored-by: Claude (Initial Force WPF Bot) <wpf-bot@initialforce.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent 02fe016 commit f703b12
1 file changed
Lines changed: 73 additions & 42 deletions
Lines changed: 73 additions & 42 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
664 | 664 | | |
665 | 665 | | |
666 | 666 | | |
667 | | - | |
668 | | - | |
669 | | - | |
| 667 | + | |
670 | 668 | | |
671 | 669 | | |
672 | | - | |
673 | 670 | | |
674 | 671 | | |
675 | 672 | | |
676 | | - | |
677 | | - | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
678 | 682 | | |
679 | 683 | | |
680 | 684 | | |
| |||
969 | 973 | | |
970 | 974 | | |
971 | 975 | | |
972 | | - | |
973 | | - | |
974 | | - | |
975 | | - | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
976 | 984 | | |
977 | | - | |
978 | | - | |
979 | | - | |
980 | | - | |
981 | | - | |
982 | | - | |
983 | | - | |
984 | | - | |
985 | | - | |
986 | | - | |
987 | | - | |
988 | | - | |
989 | | - | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
990 | 990 | | |
991 | | - | |
992 | | - | |
993 | | - | |
994 | | - | |
995 | | - | |
996 | | - | |
997 | | - | |
998 | | - | |
| 991 | + | |
999 | 992 | | |
1000 | | - | |
1001 | | - | |
1002 | 993 | | |
1003 | | - | |
| 994 | + | |
1004 | 995 | | |
1005 | | - | |
| 996 | + | |
1006 | 997 | | |
1007 | 998 | | |
1008 | 999 | | |
1009 | | - | |
| 1000 | + | |
1010 | 1001 | | |
1011 | 1002 | | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
1012 | 1007 | | |
1013 | | - | |
1014 | | - | |
1015 | | - | |
1016 | | - | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
1017 | 1030 | | |
1018 | | - | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
1019 | 1037 | | |
1020 | | - | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
1021 | 1052 | | |
1022 | 1053 | | |
1023 | 1054 | | |
| |||
0 commit comments