The Texture tab was added to the two UIA goldens (*.expected.json) but not
to their AT-SPI counterparts (*.atspi.json), whose `tabs` list also feeds
verify-atspi-tree-golden.py's --strict-extra allowlist. Windows stayed
green while both Linux golden fixtures failed with
"EXTRA unlisted interactive 'Texture' role=page tab".
TextureView in a Windows tray panel (TaoStandalonePopupHost) rendered a
permanently empty Box: that host was never given the plumbing its macOS and
Linux twins got, so LocalTaoPopupHostWindows was null inside its scene. It
cannot implement TaoPopupHostWindows either — it is not a popup host.
Introduce TaoWindowsTextureHost, the narrow Windows counterpart of
TaoGlTextureHost / TaoMetalTextureHost (host HWND, Skia context, requestRedraw),
provided by the window scene and by the tray panel; WindowsTextureView consumes
it instead of the popup host. Windows popup layers replay their parent's locals,
so they inherit it and keep sharing the host scene's context, as before.
The import registry now keys on the Skia context rather than the parent HWND.
Every Windows surface shares one ANGLE EGLContext but not one DirectContext:
the tray panel builds its own and has no HWND of its own, so an hwnd-keyed
registry would have handed it a window's image (hwnd 0 for both) and Skia would
have silently dropped the draw. Panels also release their imports before closing
their context, like the Linux surfaces.
Also:
- A skipped keyed-mutex copy now schedules a retry redraw. Nothing else would
invalidate — markFrameAvailable for that stamp already fired and the producer
may go idle — so one contended copy froze the view on the previous frame. The
import-time priming copy gets the same timeout as the per-frame path instead
of 0, for the same reason.
- The per-controller stamp maps (Windows 'copied', macOS 'consumed') hold weak
keys. An import outlives any single view, so strong keys pinned the controller
of every view that ever drew through it; a screen creating one controller per
item over a shared source grew them without bound.
- DmaBufTestTextureProducer.source is documented as invalid after close(): the
fd it carries is closed there and the OS may recycle it.
- nucleus_tao_texture.c releases its d3d11.dll module handle on every path.
- Drop a stale comment claiming there is no Linux TextureView producer.
The tab bar gained a Texture entry with this PR, so the AT-SPI/UIA golden
tree verifiers flagged it as an unlisted interactive node (pre-merge's
tao-a11y job, already red on the previous commit). Adds it to both golden
tab lists and to the per-tab tables of the all-tabs probes; the asserted
strings are the contentScale/filterQuality labels, the only ones identical
on every platform — the producer summary line differs, and Linux has no
producer at all.
Fixes the `tao-a11y` CI flake — `[FAIL] Increment carries the 'focused'
state (got [])` in `verify-atspi-keyboard.py`. It predates this branch and
also hits `main` (runs 30905167204, 30835144173, 30795312700 all failed on
this one assertion, ~1 in 3).
Root cause is in the log the failing job dumps, not in the probe:
Exception in thread "Thread-39" java.lang.IllegalArgumentException:
Detected multithreaded access to SnapshotStateObserver:
previousThreadId=1), currentThread={id=114, name=Thread-39}
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads
at androidx.compose.foundation.FocusableNode.retrievePinnableContainer
at androidx.compose.foundation.FocusableNode.onFocusStateChange
at androidx.compose.ui.focus.FocusTransactionsKt.performRequestFocus
at androidx.compose.foundation.FocusableNode$applySemantics$1.invoke
`onActionInvoked` ran the handler lambdas on whatever thread called in, and
AccessKit calls in from its own worker — instrumenting the upcall shows a
different one every time (Thread-7 … Thread-11 locally, Thread-39 on the
runner) while the Tao main thread is `main`/id=1. Plain state writes survive
that, which is why click and increment always passed, but
`SemanticsActions.RequestFocus` walks Compose's focus machinery into
`observeReads`, whose SnapshotStateObserver belongs to the main thread. When
the main thread happens to be observing at that moment the check throws, the
focus transaction aborts half-applied, and no node ends up focused at all —
exactly what the assertion reports. On an idle desktop the collision window
is narrow (the assertion passes 3/3 locally on the pre-fix build); a loaded
CI runner hits it regularly.
- `withHandlersOnMainThread` marshals every a11y action onto
`TaoMainDispatcher` — inline when already on the Tao main thread (macOS AX,
where nothing changes), posted plus a redraw wake otherwise, since the
dispatcher only drains on `MAIN_EVENTS_CLEARED` and `pump()` sends the
apply notifications itself. All handlers, not just focus: Compose UI's
contract is single-threaded for every one of these paths.
- `verify-atspi-keyboard.py` polls for the 'focused' state instead of reading
it once after a fixed 400 ms sleep. Not the root cause, but that chain
(D-Bus round trip → Compose focus request → next frame → AccessKit push)
is asynchronous and the marshalling adds one loop tick to it; every other
timing-sensitive assertion in the probe already polls.
Verified on a GNOME/XWayland session: all four AT-SPI probes green
(`verify-atspi`, `-events`, `-all-tabs`, `-keyboard` — including the flaky
assertion, `focused accessibles: ['Increment']`) with zero multithreaded-access
exceptions in the app log.
Replace hand-rolled nucleus_tao_a11y COM projector with AccessKit on Windows,
share wire/decode path with Linux, and add golden tree + keyboard/event CI probes.
Surfaced by the advanced a11y probe: Compose CustomAccessibilityActions were
reachable from VoiceOver (macOS rotor) but invisible to Orca — the vendored
accesskit_atspi_common only exposed the synthetic 'click' action. n_actions/
get_action_name/do_action now append the node's custom actions after the
click slot; do_action routes them as Action::CustomAction with the per-node
index, which the embedder side (linux/a11y.rs) already dispatches to
Kotlin's customActions list. Linux natives rebuilt by CI.
Probes now assert the advanced A11yTab sections, not just the basics:
- disabled button: role survives AND the disabled state projects
(AX: AXButton + AXEnabled=false; UIA: IsEnabled=false; AT-SPI: no
STATE_ENABLED)
- custom actions: labels ride the a11y channel and are performable
(AX action names; AT-SPI doAction('Mark as read') round-trip -> counter)
- live region: pressing 'Update status' surfaces the new status text
- toggleable without an explicit role: STATE_CHECKABLE / TogglePattern
- heading role (AT-SPI)
- slider Value interface set/read (AT-SPI)
Also fixes a REAL a11y bug the new assertions caught: A11yButton's disabled
branch used semantics{} without mergeDescendants (unlike the enabled
clickable path), so the label stayed a separate enabled static-text node and
the disabled button projected unlabeled — VoiceOver read 'Cannot press' as
plain active text. Local AX probe: 7/7 PASS.
Mirrors the macOS TCC self-skip: on hosted runners the software display
adapter yields no WGL context, the scene never attaches and only the OS
nonclient elements exist — nothing assertable, loud SKIPPED, exit 0. Any
partial exposure with failing assertions still fails hard, and the job is
gating again (continue-on-error removed).
FindAll uses the filtered control view, which never descended into the
custom UIA provider on the runner (only the 7 nonclient elements showed).
scripts/dump-uia-tree.ps1 — validated during manual provider bring-up —
walks TreeWalker.RawViewWalker; the probe now does the same, and dumps the
raw tree on failure.
- test-graalvm/tao-native-tests: execute the binary from graalvm/output
(the packaging step extracts skiko/jawt/icudtl next to it; the bare
nativeCompile artifact lacks them — Windows failed unpacking skiko).
- verify-uia.ps1: dump the UIA descendant names when the A11y content is
missing, and try invoking the 'A11y' tab through UIA as a fallback.
The prebuild step starts the Gradle daemon without NUCLEUS_DEMO_TAB; the
launch step then reuses that daemon and JavaExec inherits ITS environment,
so the app could open on the Demo tab instead of A11y (observed on the
Windows runner). --no-daemon makes the app JVM inherit the step env
directly. The UIA probe also fails cleanly now instead of null-crashing
when an element is missing.
All three first-run failures were the same race: the probe's 120s window was
consumed by the cold gradle compile of tao-demo on a fresh runner. Build the
jar in a prior step and give the probes 300s.
GraalVM native-image testing (examples/tao-native-test):
- New runner app (nucleus plugin, graalvm enabled) that compiles the whole
Tao test pyramid into a native binary and RUNS it — the first place a
produced native image is actually executed in CI (test-graalvm.yaml only
compiled nucleus-demo before). Consumes decorated-window-tao's compiled
test classes via a taoTestArtifacts configuration.
- TaoSceneTestBattery: generated reflection-free registry of the stage-1
offscreen suite (60 cases) — JUnit discovery needs reflection metadata,
direct calls need none.
- Two process modes (battery / headful): the offscreen battery's global
Compose runtime state prevents the Tao loop from starting in the same
process, so CI compiles once and runs the binary once per mode.
- New tao-native-tests job in test-graalvm.yaml (3-OS matrix, Xvfb+openbox
on Linux). Local macOS: battery 60/60 and headful 6/6 with native=true.
Native accessibility protocol (scripts/ci/ + tao-a11y job in pre-merge.yaml):
- Verifies the OS-level a11y projection from the OUTSIDE, via the same
client APIs assistive tech uses, against tao-demo's A11y tab
(NUCLEUS_DEMO_TAB env added to tao-demo for direct navigation):
- Windows (hard gate): verify-uia.ps1 — UIA tree exposure, InvokePattern
round-trip (Increment -> 'click counter 1'), TogglePattern, RangeValue.
Derived from scripts/dump-uia-tree.ps1 / uia-action.ps1.
- Linux (hard gate): verify-atspi.py — pyatspi walk over the AT-SPI bus
(org.a11y.Status.IsEnabled flipped like scripts/a11y-linux-iter.sh),
element exposure + doAction round-trip.
- macOS: verify-ax.swift — AX client dump (axdump/axtool derived);
self-skips on runners without the TCC Accessibility permission.
Verified locally: 4/4 elements exposed, 53 named accessibles.