Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/validation/accessibility-semantic-live-regions-20260919.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Accessibility semantic live-region publication boundary

WebScene issue #546 adds a bounded, one-way semantic live-region provider for
the native peers tracked by AppScene issue #185. WebScene publishes immutable
UTF-8 event leases. It does not create platform accessibility objects or call
AppKit, UI Automation, AT-SPI, screen-reader or announcement APIs.

## Contract

`webscene_engine_take_semantic_live_events_v1` removes the next completed
worker publication and returns an immutable lease which may outlive its engine.
Each event contains a monotonic sequence, top-document and nested-frame
generations, semantic identity, live-region DOM identity, role, politeness,
coalescing flags and one UTF-8 text slice. AppScene can compare those numeric
generations with its current semantic peer before delivering an announcement.

The supported DOM subset recognizes explicit `aria-live="polite"` and
`aria-live="assertive"`, plus the implicit semantics of `status`, `alert` and
`log`. Explicit `aria-live="off"` disables the region. `status` and `alert`
default to atomic; `log` defaults to additions; other live regions default to
additions and text. Valid `aria-atomic` and `aria-relevant` tokens override
those defaults. An `aria-busy="true"` region or ancestor retains the pre-busy
baseline and emits one coalesced event after busy clears. A newly connected
alert may publish its initial content; other new regions establish a baseline.

Worker checkpoints compare final composed-tree state. Regions publish in
composed-tree order. Within a non-atomic region, additions and text changes use
current preorder, followed by relevant removals in prior preorder. Replacing a
text node at the same position is classified as text. Exact repeated state is
suppressed by semantic identity, changed-node identity, cause flags and text.
Nested live regions own their own descendants, so a parent does not duplicate
their text.

Hidden, `aria-hidden`, `display:none`, visibility/content-visibility hidden,
inert, detached and stale-document regions never enter an event. Navigation
and low-memory requests synchronously retire queued events. The worker checks
the iteration-start document epoch before publication, closing a race with
host-side retirement. Each complete checkpoint also prunes queued events whose
document generation or live-region semantic identity is no longer current.
Navigation resets runtime baselines for the new document;
low memory rebuilds the current document baseline without replaying alerts.
Teardown clears both pending actions and live-region events. Existing leases
remain valid because they own their event and string storage.

## Bounds and loss behavior

- 256 queued events, with deterministic oldest-first eviction;
- 64 events per take lease;
- 64 KiB of valid UTF-8 per event;
- 1 MiB of aggregate queued UTF-8;
- 256 live regions, 4,096 text fragments per region and 16,384 bounded DOM
visits per worker checkpoint;
- explicit batch loss flag and saturated dropped-event count;
- one event per changed region per worker checkpoint.

If a checkpoint exceeds its scan, region or fragment budget, WebScene retains
the preceding baseline, retires queued events and publishes nothing from the
partial scan. Queue
eviction preserves the newest events and reports the exact bounded loss on the
next take.

## Authored qualification sources

- `semantic_live_region_header_tests.c` pins the C11 event, lease and cap ABI.
- `native_v8_runtime_semantic_live_region_tests.inc` covers terminal, Problems,
Tasks and Chat ordering; polite/assertive/status/alert/log behavior;
atomic/relevant/busy coalescing; duplicate, hidden and detached suppression;
sequence order; navigation, low-memory and teardown retirement; queue/lease
caps and a bounded worker-time gate.
- `accessibility-semantic-live-regions-source.html` and its dedicated profile
retain the Chromium-shaped browser DOM oracle.
- `native-binary-interop.test.mjs` pins take/release export visibility.

Per the implementation-only instruction, these sources were authored but not
executed. Only `git diff --check` is used for this slice.

Incremental semantic tree deltas, platform peer mapping, announcement API
delivery, interruption policy, user preference integration and packaged
VoiceOver, Narrator and Orca acceptance remain in later WebScene/AppScene work.
8 changes: 8 additions & 0 deletions experiments/WebScene.NativeEngine.Probe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ include(CTest)
include(FetchContent)

if(BUILD_TESTING)
add_executable(webscene_semantic_live_region_header_tests
tests/semantic_live_region_header_tests.c)
target_compile_features(webscene_semantic_live_region_header_tests PRIVATE c_std_11)
target_include_directories(webscene_semantic_live_region_header_tests PRIVATE native)
add_test(NAME webscene_semantic_live_region_header_tests
COMMAND webscene_semantic_live_region_header_tests)
set_tests_properties(webscene_semantic_live_region_header_tests PROPERTIES
LABELS "native;accessibility;live-region;abi;c11" TIMEOUT 10)
add_executable(webscene_semantic_action_header_tests
tests/semantic_action_header_tests.c)
target_compile_features(webscene_semantic_action_header_tests PRIVATE c_std_11)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,104 @@ struct webscene_engine final {
semantic_action_payload_bytes_ = 0U;
semantic_actions_pending_.store(false, std::memory_order_release);
}
void publish_semantic_live_events_v1(
webscene_native::semantic_live_capture_data_v1 capture,
uint64_t document_epoch) {
auto notify = false;
{
std::lock_guard lock(semantic_live_mutex_);
if (document_epoch != semantic_document_epoch_.load(
std::memory_order_acquire)) return;
if (!capture.complete) {
semantic_live_dropped_events_ = std::min<uint64_t>(
UINT32_MAX,
semantic_live_dropped_events_ + semantic_live_events_.size());
semantic_live_events_.clear();
semantic_live_text_bytes_ = 0U;
return;
}
std::erase_if(semantic_live_events_, [&](const auto& event) {
const auto stale = event.top_document_generation
!= capture.top_document_generation
|| !std::binary_search(
capture.live_region_semantic_ids.begin(),
capture.live_region_semantic_ids.end(),
event.semantic_id);
if (stale) {
semantic_live_text_bytes_ -= event.text.size();
semantic_live_dropped_events_ = std::min<uint64_t>(
UINT32_MAX, semantic_live_dropped_events_ + 1U);
}
return stale;
});
for (auto& event : capture.events) {
if (event.text.empty()) continue;
if (event.text.size()
> WEBSCENE_SEMANTIC_LIVE_MAXIMUM_TEXT_BYTES_V1) {
auto copied = static_cast<size_t>(
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_TEXT_BYTES_V1);
while (copied > 0U
&& (static_cast<unsigned char>(event.text[copied])
& 0xc0U) == 0x80U) --copied;
event.text.resize(copied);
event.flags |= WEBSCENE_SEMANTIC_LIVE_TEXT_TRUNCATED_V1;
}
while (!semantic_live_events_.empty()
&& (semantic_live_events_.size()
>= WEBSCENE_SEMANTIC_LIVE_MAXIMUM_PENDING_EVENTS_V1
|| event.text.size()
> WEBSCENE_SEMANTIC_LIVE_MAXIMUM_QUEUED_TEXT_BYTES_V1
- semantic_live_text_bytes_)) {
semantic_live_text_bytes_ -=
semantic_live_events_.front().text.size();
semantic_live_events_.pop_front();
semantic_live_dropped_events_ = std::min<uint64_t>(
UINT32_MAX, semantic_live_dropped_events_ + 1U);
}
event.sequence = next_semantic_live_sequence_++;
semantic_live_text_bytes_ += event.text.size();
semantic_live_events_.push_back(std::move(event));
notify = true;
}
}
if (notify) notify_host_work();
}
std::shared_ptr<webscene_native::semantic_live_batch_data_v1>
take_semantic_live_events_v1() {
std::lock_guard lock(semantic_live_mutex_);
if (semantic_live_events_.empty()
&& semantic_live_dropped_events_ == 0U) return {};
auto batch = std::make_shared<
webscene_native::semantic_live_batch_data_v1>();
batch->batch_generation = next_semantic_live_batch_generation_++;
batch->dropped_event_count = static_cast<uint32_t>(
semantic_live_dropped_events_);
if (batch->dropped_event_count != 0U) {
batch->flags |= WEBSCENE_SEMANTIC_LIVE_BATCH_DROPPED_EVENTS_V1;
}
semantic_live_dropped_events_ = 0U;
const auto count = std::min<size_t>(
semantic_live_events_.size(),
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_EVENTS_PER_LEASE_V1);
batch->events.reserve(count);
batch->strings.reserve(std::min<size_t>(
semantic_live_text_bytes_,
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_QUEUED_TEXT_BYTES_V1));
for (size_t index = 0U; index < count; ++index) {
semantic_live_text_bytes_ -= semantic_live_events_.front().text.size();
batch->append(std::move(semantic_live_events_.front()));
semantic_live_events_.pop_front();
}
return batch;
}
void retire_semantic_live_events_v1() {
std::lock_guard lock(semantic_live_mutex_);
semantic_live_dropped_events_ = std::min<uint64_t>(
UINT32_MAX,
semantic_live_dropped_events_ + semantic_live_events_.size());
semantic_live_events_.clear();
semantic_live_text_bytes_ = 0U;
}
void set_work_available_callback(webscene_work_available_callback_v1 callback, void* data) {
std::lock_guard lock(host_observer_mutex_);
host_observer_ = callback;
Expand Down Expand Up @@ -821,6 +919,13 @@ struct webscene_engine final {
std::mutex semantic_action_mutex_;
size_t semantic_action_payload_bytes_{0U};
std::atomic<bool> semantic_actions_pending_{false};
std::deque<webscene_native::semantic_live_event_data_v1>
semantic_live_events_;
std::mutex semantic_live_mutex_;
size_t semantic_live_text_bytes_{0U};
uint64_t semantic_live_dropped_events_{0U};
uint64_t next_semantic_live_sequence_{1U};
uint64_t next_semantic_live_batch_generation_{1U};
std::atomic<bool> ordered_scene_consumer_{false};
std::atomic<bool> producer_gpu_wait_consumer_{false};
#if defined(WEBSCENE_NATIVE_ENGINE_ENABLE_GRAPHICS)
Expand Down Expand Up @@ -2011,6 +2116,27 @@ struct semantic_snapshot_lease_v1 final {
view.lease_token = this;
}
};

struct semantic_live_batch_lease_v1 final {
std::shared_ptr<webscene_native::semantic_live_batch_data_v1> value;
webscene_semantic_live_batch_view_v1 view{};

explicit semantic_live_batch_lease_v1(
std::shared_ptr<webscene_native::semantic_live_batch_data_v1> batch)
: value(std::move(batch))
{
view.struct_size = sizeof(view);
view.version = 1U;
view.batch_generation = value->batch_generation;
view.flags = value->flags;
view.dropped_event_count = value->dropped_event_count;
view.events = value->events.empty() ? nullptr : value->events.data();
view.event_count = static_cast<uint32_t>(value->events.size());
view.string_bytes = value->strings.empty() ? nullptr : value->strings.data();
view.string_byte_count = static_cast<uint32_t>(value->strings.size());
view.lease_token = this;
}
};
} // namespace

const webscene_semantic_snapshot_view_v1*
Expand Down Expand Up @@ -2050,6 +2176,29 @@ uint32_t webscene_engine_request_semantic_action_v1(
}
}

const webscene_semantic_live_batch_view_v1*
webscene_engine_take_semantic_live_events_v1(webscene_engine* engine)
{
if (engine == nullptr) return nullptr;
try {
auto value = engine->take_semantic_live_events_v1();
if (!value) return nullptr;
auto* lease = new semantic_live_batch_lease_v1(std::move(value));
return &lease->view;
} catch (...) {
return nullptr;
}
}

void webscene_semantic_live_batch_release_v1(
const webscene_semantic_live_batch_view_v1* batch)
{
if (batch == nullptr || batch->version != 1U
|| batch->struct_size < sizeof(*batch)
|| batch->lease_token == nullptr) return;
delete static_cast<const semantic_live_batch_lease_v1*>(batch->lease_token);
}

namespace {
struct scene_lease_v3 {
webscene_scene_lease cpu;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ _webscene_engine_set_accessibility_preferences_v1
_webscene_engine_acquire_semantic_snapshot_v1
_webscene_semantic_snapshot_release_v1
_webscene_engine_request_semantic_action_v1
_webscene_engine_take_semantic_live_events_v1
_webscene_semantic_live_batch_release_v1
_webscene_engine_set_visible
_webscene_engine_set_window_focused_v1
_webscene_engine_set_window_fullscreen_v1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,33 @@ typedef enum webscene_semantic_action_admission_v1 {
WEBSCENE_SEMANTIC_ACTION_PAYLOAD_TOO_LARGE_V1 = 5
} webscene_semantic_action_admission_v1;

typedef enum webscene_semantic_live_region_role_v1 {
WEBSCENE_SEMANTIC_LIVE_REGION_GENERIC_V1 = 0,
WEBSCENE_SEMANTIC_LIVE_REGION_STATUS_V1 = 1,
WEBSCENE_SEMANTIC_LIVE_REGION_ALERT_V1 = 2,
WEBSCENE_SEMANTIC_LIVE_REGION_LOG_V1 = 3
} webscene_semantic_live_region_role_v1;

typedef enum webscene_semantic_live_politeness_v1 {
WEBSCENE_SEMANTIC_LIVE_POLITE_V1 = 1,
WEBSCENE_SEMANTIC_LIVE_ASSERTIVE_V1 = 2
} webscene_semantic_live_politeness_v1;

enum {
WEBSCENE_SEMANTIC_LIVE_RELEVANT_ADDITIONS_V1 = 1U << 0U,
WEBSCENE_SEMANTIC_LIVE_RELEVANT_TEXT_V1 = 1U << 1U,
WEBSCENE_SEMANTIC_LIVE_RELEVANT_REMOVALS_V1 = 1U << 2U,
WEBSCENE_SEMANTIC_LIVE_ATOMIC_V1 = 1U << 3U,
WEBSCENE_SEMANTIC_LIVE_BUSY_COALESCED_V1 = 1U << 4U,
WEBSCENE_SEMANTIC_LIVE_INITIAL_ALERT_V1 = 1U << 5U,
WEBSCENE_SEMANTIC_LIVE_TEXT_TRUNCATED_V1 = 1U << 6U,
WEBSCENE_SEMANTIC_LIVE_BATCH_DROPPED_EVENTS_V1 = 1U << 0U,
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_PENDING_EVENTS_V1 = 256U,
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_EVENTS_PER_LEASE_V1 = 64U,
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_TEXT_BYTES_V1 = 64U * 1024U,
WEBSCENE_SEMANTIC_LIVE_MAXIMUM_QUEUED_TEXT_BYTES_V1 = 1024U * 1024U
};

typedef struct webscene_semantic_string_v1 {
uint32_t offset;
uint32_t length;
Expand Down Expand Up @@ -228,6 +255,47 @@ typedef struct webscene_semantic_snapshot_view_v1 {
const void* lease_token;
} webscene_semantic_snapshot_view_v1;

/*
* One platform-neutral live-region change. text is a UTF-8 slice into its
* enclosing batch. Sequence and document/frame generations let a native peer
* preserve order and reject a publication after navigation. The semantic and
* DOM identities name the live-region root, not a platform accessibility
* object. Relevant bits describe the coalesced causes represented by text.
*/
typedef struct webscene_semantic_live_event_v1 {
uint32_t struct_size;
uint32_t version;
uint64_t sequence;
uint64_t top_document_generation;
uint64_t frame_generation;
uint64_t semantic_id;
uint32_t frame_owner_dom_node_id;
uint32_t dom_node_id;
uint32_t role;
uint32_t politeness;
uint32_t flags;
webscene_semantic_string_v1 text;
} webscene_semantic_live_event_v1;

/*
* Immutable take lease. Taking removes at most 64 queued events. A lease may
* outlive the engine. dropped_event_count is the deterministic loss observed
* since the preceding successful take, including navigation/low-memory
* retirement and oldest-first queue eviction.
*/
typedef struct webscene_semantic_live_batch_view_v1 {
uint32_t struct_size;
uint32_t version;
uint64_t batch_generation;
uint32_t flags;
uint32_t dropped_event_count;
const webscene_semantic_live_event_v1* events;
uint32_t event_count;
const char* string_bytes;
uint32_t string_byte_count;
const void* lease_token;
} webscene_semantic_live_batch_view_v1;

/*
* Host-to-DOM semantic action. The host copies snapshot_generation and
* semantic_id from one acquired node. A newer publication may route the same
Expand Down Expand Up @@ -1611,6 +1679,11 @@ WEBSCENE_API void webscene_semantic_snapshot_release_v1(
WEBSCENE_API uint32_t webscene_engine_request_semantic_action_v1(
webscene_engine* engine,
const webscene_semantic_action_request_v1* request);
/* Takes the next bounded immutable live-region batch, or null when empty. */
WEBSCENE_API const webscene_semantic_live_batch_view_v1*
webscene_engine_take_semantic_live_events_v1(webscene_engine* engine);
WEBSCENE_API void webscene_semantic_live_batch_release_v1(
const webscene_semantic_live_batch_view_v1* batch);
/* Returns the CSS cursor resolved at the latest hit-tested pointer position. */
WEBSCENE_API uint32_t webscene_engine_get_cursor(const webscene_engine* engine);
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
std::memory_order_release);
}
retire_semantic_actions_v1();
retire_semantic_live_events_v1();
low_memory_requested_.store(true, std::memory_order_release);
signal_worker();
return true;
Expand Down Expand Up @@ -870,6 +871,7 @@
std::memory_order_release);
}
retire_semantic_actions_v1();
retire_semantic_live_events_v1();
semantic_snapshot_requested_.store(true, std::memory_order_release);
script_work_.emplace_back(url_request{
std::string(value, length),
Expand Down
Loading
Loading