From 824cc5a96f8a6f666a5c963897c8ab9076b48373 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Mon, 17 Aug 2026 15:20:57 +0200 Subject: kapi: add power management support to drivers --- kernel/kapi/devices/driver.cpp | 12 +++ kernel/kapi/devices/power.cpp | 93 +++++++++++++++++ kernel/kapi/devices/power.tests.cpp | 203 ++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 kernel/kapi/devices/power.cpp create mode 100644 kernel/kapi/devices/power.tests.cpp (limited to 'kernel/kapi') diff --git a/kernel/kapi/devices/driver.cpp b/kernel/kapi/devices/driver.cpp index 4f0ce0e4..5ca92dc8 100644 --- a/kernel/kapi/devices/driver.cpp +++ b/kernel/kapi/devices/driver.cpp @@ -1,6 +1,8 @@ #include #include +#include + #include #include @@ -17,6 +19,16 @@ namespace kapi::devices return std::nullopt; } + auto driver::suspend(device &) -> kstd::result + { + return kstd::success(); + } + + auto driver::resume(device &) -> kstd::result + { + return kstd::success(); + } + auto driver::query_facet(kapi::capabilities::facet_id) -> void * { return nullptr; diff --git a/kernel/kapi/devices/power.cpp b/kernel/kapi/devices/power.cpp new file mode 100644 index 00000000..3bc66848 --- /dev/null +++ b/kernel/kapi/devices/power.cpp @@ -0,0 +1,93 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +namespace kapi::devices +{ + + namespace + { + auto resume_subtree(device & node) -> void; + + // NOLINTNEXTLINE(misc-no-recursion) + auto suspend_subtree(device & node) -> kstd::result + { + auto suspended_children = kstd::vector>{}; + + if (auto as_bus = node.facet()) + { + for (auto const & child : as_bus->children()) + { + auto result = suspend_subtree(*child); + if (!result) + { + for (auto const & already_suspended : std::views::reverse(suspended_children)) + { + resume_subtree(*already_suspended); + } + + return result; + } + + suspended_children.push_back(child); + } + } + + if (auto driver = node.bound_driver()) + { + auto result = driver->suspend(node); + if (!result) + { + for (auto const & already_suspended : std::views::reverse(suspended_children)) + { + resume_subtree(*already_suspended); + } + + return result; + } + } + + return kstd::success(); + } + + // NOLINTNEXTLINE(misc-no-recursion) + auto resume_subtree(device & node) -> void + { + if (auto driver = node.bound_driver()) + { + auto result = driver->resume(node); + if (!result) + { + kstd::println(kstd::print_sink::stderr, "[OS:PM] failed to resume device '{}': {}", node.name(), + result.error().message()); + } + } + + if (auto as_bus = node.facet()) + { + for (auto const & child : as_bus->children()) + { + resume_subtree(*child); + } + } + } + } // namespace + + auto suspend_tree(bus & root) -> kstd::result + { + return suspend_subtree(root); + } + + auto resume_tree(bus & root) -> void + { + resume_subtree(root); + } + +} // namespace kapi::devices \ No newline at end of file diff --git a/kernel/kapi/devices/power.tests.cpp b/kernel/kapi/devices/power.tests.cpp new file mode 100644 index 00000000..8f522040 --- /dev/null +++ b/kernel/kapi/devices/power.tests.cpp @@ -0,0 +1,203 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + + struct test_device : kapi::devices::device + { + using kapi::devices::device::device; + }; + + struct recording_driver final : kapi::devices::driver + { + explicit recording_driver(kstd::vector & log, kstd::string fail_on_suspend = {}) + : m_log{&log} + , m_fail_on_suspend{fail_on_suspend} + {} + + [[nodiscard]] auto probe(kapi::devices::device &) -> kstd::result override + { + return kstd::success(); + } + + auto unbind(kapi::devices::device &) -> void override {} + + [[nodiscard]] auto suspend(kapi::devices::device & device) -> kstd::result override + { + if (!m_fail_on_suspend.empty() && device.name() == m_fail_on_suspend) + { + return kstd::failure(make_error_code(kstd::errc::io_error)); + } + + m_log->push_back(kstd::format("suspend:{}", device.name())); + return kstd::success(); + } + + [[nodiscard]] auto resume(kapi::devices::device & device) -> kstd::result override + { + m_log->push_back(kstd::format("resume:{}", device.name())); + return kstd::success(); + } + + [[nodiscard]] auto name() const noexcept -> std::string_view override + { + return "recording_driver"; + } + + private: + kstd::vector * m_log; + kstd::string m_fail_on_suspend; + }; + + auto index_of(kstd::vector const & log, std::string_view entry) -> std::size_t + { + auto found = std::ranges::find(log, entry); + REQUIRE(found != log.end()); + return static_cast(found - log.begin()); + } + +} // namespace + +SCENARIO("Suspension and resumption happens in the correct order", "[devices][power]") +{ + GIVEN("a bus with two bound lead devices, itself bound to its own driver, attached to a root") + { + auto log = kstd::vector{}; + + auto root = kstd::make_shared("power_test_root"); + auto middle = kstd::make_shared("power_test_middle"); + auto leaf_1 = kstd::make_shared("power_test_leaf_1"); + auto leaf_2 = kstd::make_shared("power_test_leaf_2"); + + auto middle_driver = kstd::make_shared(log); + auto leaf_1_driver = kstd::make_shared(log); + auto leaf_2_driver = kstd::make_shared(log); + + root->add_child(middle); + middle->add_child(leaf_1); + middle->add_child(leaf_2); + + middle->bind_driver(middle_driver); + middle->set_state(kapi::devices::state::bound); + + leaf_1->bind_driver(leaf_1_driver); + leaf_1->set_state(kapi::devices::state::bound); + + leaf_2->bind_driver(leaf_2_driver); + leaf_2->set_state(kapi::devices::state::bound); + + WHEN("the bus is suspended") + { + auto result = kapi::devices::suspend_tree(*root); + + THEN("it succeeds") + { + REQUIRE(result); + } + + THEN("the leaves were suspended before the middle bus") + { + REQUIRE(log.size() == 3); + + auto middle_index = index_of(log, "suspend:power_test_middle"); + auto leaf_1_index = index_of(log, "suspend:power_test_leaf_1"); + auto leaf_2_index = index_of(log, "suspend:power_test_leaf_2"); + + REQUIRE(leaf_1_index < middle_index); + REQUIRE(leaf_2_index < middle_index); + } + + AND_WHEN("the bus is resumed") + { + kapi::devices::resume_tree(*root); + + THEN("the leaves were resumed after the middle bus") + { + auto middle_index = index_of(log, "resume:power_test_middle"); + auto leaf_1_index = index_of(log, "resume:power_test_leaf_1"); + auto leaf_2_index = index_of(log, "resume:power_test_leaf_2"); + + REQUIRE(middle_index < leaf_1_index); + REQUIRE(middle_index < leaf_2_index); + } + } + } + } + + GIVEN("a subtree where one leaf will fail to suspend") + { + auto log = kstd::vector{}; + + auto root = kstd::make_shared("power_test_root_failing"); + auto middle = kstd::make_shared("power_test_middle_failing"); + auto leaf_1 = kstd::make_shared("power_test_leaf_1_failing"); + auto leaf_2 = kstd::make_shared("power_test_leaf_2_failing"); + + auto middle_driver = kstd::make_shared(log); + // leaf_1 suspends normally; leaf_2 is told to fail. + auto leaf_1_driver = kstd::make_shared(log); + auto leaf_2_driver = kstd::make_shared(log, kstd::string{"power_test_leaf_2_failing"}); + + root->add_child(middle); + middle->add_child(leaf_1); + middle->add_child(leaf_2); + + middle->bind_driver(middle_driver); + middle->set_state(kapi::devices::state::bound); + leaf_1->bind_driver(leaf_1_driver); + leaf_1->set_state(kapi::devices::state::bound); + leaf_2->bind_driver(leaf_2_driver); + leaf_2->set_state(kapi::devices::state::bound); + + WHEN("the subtree is suspended") + { + auto result = kapi::devices::suspend_tree(*root); + + THEN("the failure is reported") + { + REQUIRE_FALSE(result); + } + + THEN("the leaf that suspended successfully before the failure was resumed again") + { + REQUIRE(log.size() == 2); + REQUIRE(log[0] == "suspend:power_test_leaf_1_failing"); + REQUIRE(log[1] == "resume:power_test_leaf_1_failing"); + } + + THEN("the middle bus, whose child failed to suspend, was never suspended at all") + { + REQUIRE(std::ranges::find(log, "suspend:power_test_middle_failing") == log.end()); + } + } + } + + GIVEN("a bus with a child that has no bound driver at all") + { + auto root = kstd::make_shared("power_test_root_unbound"); + auto unbound_child = kstd::make_shared("power_test_unbound_child"); + + root->add_child(unbound_child); + + THEN("suspending and resuming it does not crash, and simply does nothing for that device") + { + REQUIRE(kapi::devices::suspend_tree(*root)); + REQUIRE_NOTHROW(kapi::devices::resume_tree(*root)); + } + } +} \ No newline at end of file -- cgit v1.2.3