Skip to content

Phase 0 closeout: render, AccessKit, bevy_picking#28

Merged
intendednull merged 10 commits into
mainfrom
phase-0-closeout
May 8, 2026
Merged

Phase 0 closeout: render, AccessKit, bevy_picking#28
intendednull merged 10 commits into
mainfrom
phase-0-closeout

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

Closes the three substantive deferrals from the Phase 0 self-review (docs/plans/2026-05-07-buiy-phase-0-foundations.md lines 2784–2790):

  • Render pipeline draws. BuiyNode::run builds a per-frame instance buffer from ExtractedDraws (logical-px → clip-space via to_instance), binds the static unit-quad VBO held on BuiyPipeline, and issues pass.draw(0..4, 0..n). New crates/buiy_core/src/render/instance.rs holds the POD + conversion. Shader's prior half_size = rect_size * 0.5 was buggy with the y-flipped negative rect_size.y; fixed to abs(i.rect_size) * 0.5 with regression tests.
  • AccessKit per-window adapter (bridge). crates/buiy_core/src/a11y/ gains pure translate.rs (winit-free) + adapter.rs. Bevy 0.18's accesskit_winit::Adapter::* constructors all require &ActiveEventLoop (only available inside the winit runner), so Buiy bridges into bevy::winit::accessibility::ACCESS_KIT_ADAPTERS rather than owning Adapter objects directly. push_tree_updates runs in BuiySet::A11yUpdate.after(build_tree) and pushes accesskit::TreeUpdate to every existing adapter each frame, including empty trees so AT state clears correctly when widgets disappear.
  • bevy_picking backend. New BuiyPickingBackendPlugin emits PointerHits from PointerLocation × ResolvedLayout AABBs in PreUpdate / PickingSystems::Backend. Hovered rewired as a thin layer consuming MessageReader<PointerHits>. Composed into BuiyPlugin along with bevy::picking::PickingPlugin (the sole registrar of PickingSystems sets and Messages<PointerHits>).

Plus: workspace deps (bytemuck, accesskit, accesskit_winit; bevy_picking feature on bevy), [draft][landed] flips on the original Phase 0 plan + this closeout plan, CHANGELOG entries, plan in docs/plans/2026-05-08-buiy-phase-0-closeout.md.

Architectural deviations from the plan body (forced by Bevy 0.18 reality)

  • accesskit_winit pinned to 0.29 (matches Bevy 0.18's vendored copy), not the plan's 0.31.
  • PointerHits is a Message in Bevy 0.18, not an Event; uses MessageWriter/MessageReader/Messages<T>.
  • PickingSystems::Backend, not PickSet::Backend.
  • AccessKit adapter bridge instead of plan's HashMap<WindowId, Adapter> resource — see closeout plan self-review for the full rationale.

Out of scope (explicit deferrals to v0.x sub-specs)

  • Real-GPU pixel-diff CI gate (the two #[ignore]'d render-smoke tests stay ignored).
  • Persistent buffers, atlasing, top-layer compositing — buiy-render-pipeline-design.
  • Multi-pointer arbitration, hit-target linter — buiy-input-events-design.
  • ACCNAME 1.2, full ARIA taxonomy — buiy-accessibility-design.

Test plan

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps — clean
  • cargo deny check — clean
  • cargo test --workspace — all green (~50 tests pass, 2 expected #[ignore]'d GPU smokes)
  • cargo run --example hello_button — opens window, paints button, no panic
  • Manual gate: attach a real screen reader (Orca on Linux with AT-SPI) and verify "Save, Button" announcement when Tab focuses the button. (Manual release-gate per verification.md.)
  • Manual gate: verify pixel output looks right on macOS / Windows runners (visual smoke). CI runs build + non-render tests on all three platforms.

🤖 Generated with Claude Code

intendednull and others added 10 commits May 8, 2026 01:13
Phase 0 closeout: promote three deferred workspace deps from the
speculative list to first-class entries, and enable the bevy_picking
feature on the bevy workspace dep.

- bevy: add bevy_picking to feature list
- bytemuck 1 (derive): instance-buffer POD trait impls
- accesskit 0.21 / accesskit_winit 0.29: pinned to match Bevy 0.18's
  vendored versions to keep a single copy in the dep graph
  (cargo tree -p accesskit → accesskit v0.21.1, single entry)
- buiy_core: wire all three new deps via workspace = true

Still deferred: image-compare (visual harness upgrade), thiserror
(no error-typing pressure yet).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Close the Phase 0 deferred gap: the render-graph node now builds a
per-frame instance buffer from ExtractedDraws (logical-px → clip-space
via to_instance), binds the static unit-quad VBO held on BuiyPipeline,
and issues an instanced draw(0..4, 0..n). Adds DrawData::new constructor
(non_exhaustive struct requires it for external callers), window_size
field on ExtractedDraws, and removes the resolved shader TODO block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical: shader.wgsl was passing a *signed* half-extent into the SDF
(rect_size.y is negative by design for the CPU-side y-flip), so q.y
became `abs(p.y) + h/2 + r` for every interior fragment — alpha
collapsed to 0 and rects rendered invisible. Fix: abs() the rect_size
before halving in the vertex stage. The signed rect_size remains
load-bearing for the `world` computation and for `local_uv * half_size`
in the fragment stage, where both factors flip sign together.

Also:
- Pure-CPU regression tests (sdf_rounded_rect + shader_half_size port)
  pin the half_size-must-be-positive property and document the buggy
  signed-half_size path so a regression fails loudly.
- Add to_instance_offsets_position_to_clip — the existing four tests
  all used position = Vec2::ZERO, leaving the offset arithmetic
  (`* inv_w - 1.0`, `1.0 - * inv_h`) un-exercised.
- Drop the dead `Default` derive on InstanceData (Zeroable provides
  the only construction we use; Default was never invoked).
- pipeline.rs: rewrite stale "Task 11 will fix winding" comment — that
  task IS this commit chain; the TL/BL/TR/BR strip already winds
  consistently and the deferred work is the cull_mode tightening.
- node.rs: drop the duplicated clip-space-conversion paragraph that
  contradicted the closeout paragraph below it.
- mod.rs: tighten ExtractedDraws doc comment — populated only by
  extract_buiy_draws, not externally constructed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split a11y.rs into a11y/{mod,translate,adapter}.rs. translate.rs adds
pure, winit-free A11yNodeView→AccessKit translation (build_tree_update,
to_accesskit_node, node_id_for). adapter.rs adds AccessKitAdapters
(NonSend resource) and AccessKitAdapterPlugin, which pushes TreeUpdate
payloads into bevy_winit's existing ACCESS_KIT_ADAPTERS thread-local each
frame rather than owning Adapter objects directly (Adapter::new requires
&ActiveEventLoop which is only accessible inside the winit runner callback
in Bevy 0.18). Fixes pre-existing rustdoc private-intra-doc-links warning
in render/mod.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ee, drop dead AccessKitAdapters

Three fixes from code review of 269be68:

A. Always push TreeUpdate, even on empty snapshots. The previous early-return
   suppressed pushes when the last widget was removed, leaving the AT holding
   a stale tree. accesskit_unix's update_if_active correctly diffs a root-only
   update and emits ChildRemoved events; deleting the early return restores
   that contract.

C. Order push_tree_updates .after(build_tree). Both lived in
   BuiySet::A11yUpdate without an explicit constraint; Bevy's default
   ambiguity-detection is LogLevel::Ignore, so the scheduler was free to
   reorder them. Without the .after() the push system could observe the
   previous frame's snapshot and permanently lag the AT by one frame.
   build_tree is now pub(crate) so adapter.rs can name it.

B. Drop the AccessKitAdapters resource. It tracked which windows had been
   pushed to but no code read the map for any decision. bevy_winit's
   ACCESS_KIT_ADAPTERS thread-local is the source of truth; the resource
   was dead weight. Removed the struct, the NonSendMut parameter, the
   re-export from a11y/mod.rs, the re-export from buiy/src/lib.rs, and
   rewrote the smoke test to assert against bevy_winit's thread-local.

Also added two translate-test gaps: description_round_trips and
focusable_view_has_focus_action (covers Action::Focus add-action wiring).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the AABB+window-cursor shim in update_hovered with a real
bevy_picking backend. BuiyPickingBackendPlugin runs in
PickingSystems::Backend (PreUpdate), reads PointerLocation components,
and emits PointerHits messages sorted by area (smallest = topmost).
PickingPlugin's update_hovered is rewired to consume those messages,
making Hovered a thin aggregation layer over the backend.

Bevy 0.18.1 API deviations from plan:
- PointerHits is a Message (not Event); uses MessageWriter/MessageReader.
- Location.target is NormalizedRenderTarget (not PointerTarget).
- PickSet::Backend is PickingSystems::Backend.
- BuiyPlugin now also composes bevy::picking::PickingPlugin to register
  PickingSystems sets and Messages<PointerHits> before the Buiy plugins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ckend

Two comment fixes flagged in code-quality review of 54d6073:

- update_hovered: replace incorrect "cursor-leaving-window produces an
  empty picks event which clears it" with an accurate description of
  emit_picks's continue-on-empty behavior. Calls out the Phase 0
  limitation (Hovered retains stale value when cursor leaves all Buiy
  nodes) and points to buiy-input-events-design for v0.x.
- BuiyPlugin::build: update the inline order comment to acknowledge
  bevy::picking::PickingPlugin in the slot, and explain why it must
  precede the two Buiy picking plugins (registers PickingSystems sets +
  Messages<PointerHits>).

No code changes; comments only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final-review feedback: the closeout plan's self-review row claimed "full
HashMap<WindowId, Adapter>" but the actual implementation bridges into
bevy_winit's ACCESS_KIT_ADAPTERS thread-local because Bevy 0.18 owns
adapter creation (Adapter::* constructors require &ActiveEventLoop, only
accessible from the winit runner callback). Update the self-review row
to describe the bridge accurately and call out the AccessKitAdapters
resource that was dropped during Task 4's review loop. Also tighten the
CHANGELOG bullet so it doesn't imply Buiy owns the Adapter objects.

No code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intendednull intendednull merged commit e7c843d into main May 8, 2026
6 checks passed
@intendednull intendednull deleted the phase-0-closeout branch May 8, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant