diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
| commit | c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch) | |
| tree | f6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/kernel/src/server.cpp | |
| parent | 6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff) | |
| download | unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.tar.gz unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.zip | |
Slice 4: extension host + typed bus; xdg-shell/layer-shell extracted to core extensions
The kernel now names NO concrete feature. It owns: the extension host
(install/topological activate, missing-dep/cycle = startup error), the
typed Event/Filter bus (error-isolated: a throwing extension is disabled,
never the session; RAII Subscriptions), the Host API (per-extension
facade: borrows, scene layers, event catalogue, typed services), the
public RAII Listener, and a typed surface→scene-tree registry
(Host::host_surface/scene_tree_for) that replaced the untyped
wlr_surface.data convention both extensions flagged.
- ext-xdg-shell (core): toplevel/popup lifecycle, focus-on-map,
click/tap-to-focus, pointer/touch routing incl. button+axis (the
kernel only moves the cursor and emits — a contract-doc lie caught by
user hands-on), interactive move/resize via pure GrabMachine (fixes
the request-arrives-after-release race: grab requires request ∧
button-down, release always ends it), Alt+F1 cycle,
Ctrl+Alt+Backspace terminate (labwc's default A-Escape=Exit killed
the dev session once — never again; see nested-run skill).
- ext-layer-shell (core): wlr-layer-shell v1 (proto v5) for external
clients; pure doctest-hard arrangement core; fuzzel verified visually
nested (fix: seed outputs from output_layout at activate — events-only
tracking missed pre-activation outputs; plus a scene-node double-free).
- First protocol codegen: vendored wlr-layer-shell XML + wayland-scanner
server-header propagated through kernel_dep; wlr.hpp grew a
namespace→_namespace keyword fix for the generated header.
- Glossary: 'scene layer' (user-approved). New rules earned:
parallel-wave-builds, contract-docs.
- User hands-on verified: typing, click-to-focus, drag-select, scroll,
titlebar drag-move (slow + flick), Alt+F1, fuzzel + arrows, touch tap,
Ctrl+Alt+Backspace. 68 doctest cases green, ASan/UBSan clean (our
code), idle RSS ≈73 MiB.
Diffstat (limited to 'packages/kernel/src/server.cpp')
| -rw-r--r-- | packages/kernel/src/server.cpp | 191 |
1 files changed, 168 insertions, 23 deletions
diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp index df24f1f..e3308de 100644 --- a/packages/kernel/src/server.cpp +++ b/packages/kernel/src/server.cpp @@ -3,6 +3,8 @@ #include <ctime> #include <stdexcept> #include <unistd.h> +#include <unordered_map> +#include <utility> namespace unbox::kernel { @@ -42,7 +44,16 @@ auto Server::socket_name() const -> std::string { return impl_->socket; } +void Server::install(std::unique_ptr<Extension> extension) { + impl_->install(std::move(extension)); +} + +void Server::activate_extensions() { + impl_->activate_extensions(); +} + void Server::run() { + impl_->activate_extensions(); wlr_log(WLR_INFO, "unbox running on WAYLAND_DISPLAY=%s", impl_->socket.c_str()); wl_display_run(impl_->display); } @@ -68,6 +79,27 @@ auto Server::ui_spike_orientation() const -> int { // ---- Impl lifecycle -------------------------------------------------------- +void Server::Impl::register_hook(detail::HookBase& hook) { + hook.set_sink(this); + all_hooks.push_back(&hook); +} + +void Server::Impl::disable(ExtensionId who) noexcept { + // Error isolation: a callback owned by `who` threw. Mark the extension dead + // and purge its subscriptions from every hook. Safe mid-dispatch — each + // hook tombstones now and compacts when its dispatch unwinds. + for (ExtensionSlot& slot : extensions) { + if (slot.id == who && !slot.disabled) { + slot.disabled = true; + wlr_log(WLR_ERROR, "extension '%s' disabled: a hook callback threw", + slot.extension->manifest().id.c_str()); + } + } + for (detail::HookBase* hook : all_hooks) { + hook->purge(who); + } +} + void Server::Impl::init() { wlr_log_init(WLR_INFO, nullptr); @@ -91,13 +123,12 @@ void Server::Impl::init() { scene_layout = require(wlr_scene_attach_output_layout(scene, output_layout), "wlr_scene_output_layout"); - xdg_shell = require(wlr_xdg_shell_create(display, 3), "wlr_xdg_shell"); - new_xdg_toplevel.connect(xdg_shell->events.new_toplevel, [this](void* data) { - handle_new_toplevel(static_cast<wlr_xdg_toplevel*>(data)); - }); - new_xdg_popup.connect(xdg_shell->events.new_popup, [this](void* data) { - handle_new_popup(static_cast<wlr_xdg_popup*>(data)); - }); + // Ordered z-bands. wlr_scene_tree_create appends as the top child of its + // parent, so creating background -> overlay yields exactly that stacking + // order (background lowest, overlay highest). + for (auto& layer : scene_layers) { + layer = require(wlr_scene_tree_create(&scene->tree), "wlr_scene_tree (layer)"); + } cursor = require(wlr_cursor_create(), "wlr_cursor"); wlr_cursor_attach_output_layout(cursor, output_layout); @@ -110,6 +141,24 @@ void Server::Impl::init() { seat = require(wlr_seat_create(display, "seat0"), "wlr_seat"); attach_seat_handlers(); + // Register every kernel-emitted hook with the isolation registry. + for (detail::HookBase* hook : { + static_cast<detail::HookBase*>(&ev_output_added), + static_cast<detail::HookBase*>(&ev_output_removed), + static_cast<detail::HookBase*>(&ev_pointer_motion), + static_cast<detail::HookBase*>(&ev_pointer_button), + static_cast<detail::HookBase*>(&ev_pointer_axis), + static_cast<detail::HookBase*>(&ev_pointer_frame), + static_cast<detail::HookBase*>(&ev_touch_down), + static_cast<detail::HookBase*>(&ev_touch_motion), + static_cast<detail::HookBase*>(&ev_touch_up), + static_cast<detail::HookBase*>(&ev_touch_cancel), + static_cast<detail::HookBase*>(&ev_touch_frame), + static_cast<detail::HookBase*>(&key_filter), + }) { + all_hooks.push_back(hook); // sink already set via {this} constructor + } + const char* socket_cstr = wl_display_add_socket_auto(display); if (socket_cstr == nullptr) { throw std::runtime_error("failed to add a Wayland socket"); @@ -135,11 +184,96 @@ void Server::Impl::init() { } } +// ---- Extension host -------------------------------------------------------- + +void Server::Impl::install(std::unique_ptr<Extension> extension) { + if (extensions_activated) { + throw std::runtime_error("Server::install called after activate_extensions"); + } + const std::string& id = extension->manifest().id; + for (const ExtensionSlot& slot : extensions) { + if (slot.extension->manifest().id == id) { + throw std::runtime_error("duplicate extension id: " + id); + } + } + ExtensionSlot slot; + // id 0 is the kernel; extensions start at 1. + slot.id = static_cast<ExtensionId>(extensions.size() + 1); + slot.host = std::make_unique<HostImpl>(this, slot.id); + slot.extension = std::move(extension); + extensions.push_back(std::move(slot)); +} + +void Server::Impl::activate_extensions() { + if (extensions_activated) { + return; + } + extensions_activated = true; + + // Topological sort by Manifest depends_on. Index extensions by id; ties + // (no dependency relation) are broken by tier (core before standard) then + // install order — deterministic activation. + const std::size_t n = extensions.size(); + std::unordered_map<std::string, std::size_t> by_id; + for (std::size_t i = 0; i < n; ++i) { + by_id.emplace(extensions[i].extension->manifest().id, i); + } + + // Build adjacency (dep -> dependents) and indegree; validate deps exist. + std::vector<std::vector<std::size_t>> dependents(n); + std::vector<int> indegree(n, 0); + for (std::size_t i = 0; i < n; ++i) { + const Manifest& m = extensions[i].extension->manifest(); + for (const std::string& dep : m.depends_on) { + auto it = by_id.find(dep); + if (it == by_id.end()) { + throw std::runtime_error("extension '" + m.id + + "' depends on missing extension '" + dep + "'"); + } + dependents[it->second].push_back(i); + ++indegree[i]; + } + } + + // Kahn's algorithm with a deterministic tie-break: among ready nodes pick + // the lowest (tier, install-index). A linear scan is fine for the handful + // of extensions a session installs. + auto rank = [&](std::size_t i) { + return std::pair<int, std::size_t>( + static_cast<int>(extensions[i].extension->manifest().tier), i); + }; + std::vector<bool> done(n, false); + std::vector<std::size_t> order; + order.reserve(n); + for (std::size_t step = 0; step < n; ++step) { + std::size_t pick = n; + for (std::size_t i = 0; i < n; ++i) { + if (!done[i] && indegree[i] == 0) { + if (pick == n || rank(i) < rank(pick)) { + pick = i; + } + } + } + if (pick == n) { + throw std::runtime_error("extension dependency cycle detected"); + } + done[pick] = true; + order.push_back(pick); + for (std::size_t d : dependents[pick]) { + --indegree[d]; + } + } + + // Activate in topological order. An activate() throw is FATAL (not + // isolated): a core extension that cannot start is a broken session. + for (std::size_t i : order) { + ExtensionSlot& slot = extensions[i]; + slot.extension->activate(*slot.host); + slot.activated = true; + } +} + void Server::Impl::start_ui_spike() { - // The bridge needs the wlr renderer's EGLDisplay to build its sibling - // GLES 3.2 context. Only the gles2 renderer exposes one; under the - // pixman renderer (e.g. headless CI) there is no GL path, so the spike - // stays disabled — slice-2 behaviour is preserved. if (!wlr_renderer_is_gles2(renderer)) { wlr_log(WLR_INFO, "ui-spike: renderer is not gles2; spike disabled"); return; @@ -150,24 +284,34 @@ void Server::Impl::start_ui_spike() { return; } EGLDisplay display_egl = wlr_egl_get_display(egl); - ui_spike = UiSpike::create(&scene->tree, display_egl, allocator, renderer); + // The spike sits in the overlay band so it composites above everything. + ui_spike = UiSpike::create(scene_layers[static_cast<std::size_t>(SceneLayer::overlay)], + display_egl, allocator, renderer); } void Server::Impl::shutdown() { - // Slice-3 spike: tear down before scene/renderer/allocator die (it owns - // a scene node, GL objects on a sibling context, and borrows the others). + // Destroy extensions FIRST, in reverse activation order: their RAII members + // (Subscriptions, Listeners, scene nodes) release while the wlr objects + // they borrow are still alive. Reverse of `extensions` install order is a + // safe superset of reverse-topological (a dependent installed later than + // its dependency dies first; if installed earlier, it still only borrows). + for (auto it = extensions.rbegin(); it != extensions.rend(); ++it) { + it->extension.reset(); + it->host.reset(); + } + extensions.clear(); + + // Slice-3 spike: tear down before scene/renderer/allocator die. ui_spike.reset(); if (display != nullptr) { - wl_display_destroy_clients(display); // fires toplevel/popup destroy events + wl_display_destroy_clients(display); } - // Server-level listeners must detach BEFORE the wlr objects owning their - // signals die; a wl_listener outliving its signal is a use-after-free. + // Server-level listeners detach BEFORE the wlr objects owning their signals + // die; a wl_listener outliving its signal is a use-after-free. new_output.disconnect(); new_input.disconnect(); - new_xdg_toplevel.disconnect(); - new_xdg_popup.disconnect(); cursor_motion.disconnect(); cursor_motion_absolute.disconnect(); cursor_button.disconnect(); @@ -203,7 +347,7 @@ void Server::Impl::shutdown() { renderer = nullptr; } if (backend != nullptr) { - wlr_backend_destroy(backend); // fires output + input-device destroy events + wlr_backend_destroy(backend); backend = nullptr; } if (display != nullptr) { @@ -233,12 +377,9 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) { outputs.push_back(std::move(owned)); output->frame.connect(wlr_output->events.frame, [this, output](void*) { - // Slice-3 spike: render the RMLUi document if dirty, before commit so - // its damage is picked up this frame. Cheap no-op when disabled. if (ui_spike != nullptr) { ui_spike->tick(); } - wlr_scene_output* scene_output = wlr_scene_get_scene_output(scene, output->output); wlr_scene_output_commit(scene_output, nullptr); @@ -251,6 +392,8 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) { wlr_output_commit_state(output->output, event->state); }); output->destroy.connect(wlr_output->events.destroy, [this, output](void*) { + const OutputEvent ev{output->output}; + ev_output_removed.emit(ev); // Last action: destroys `output` (and these listeners with it). outputs.remove_if([output](const auto& owned) { return owned.get() == output; }); }); @@ -260,6 +403,8 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) { wlr_scene_output_layout_add_output(scene_layout, layout_output, scene_output); wlr_log(WLR_INFO, "new output %s", wlr_output->name); + const OutputEvent ev{wlr_output}; + ev_output_added.emit(ev); } } // namespace unbox::kernel |
