| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef SRC_UI_INPUT_LIB_INPUT_REPORT_READER_READER_H_ |
| #define SRC_UI_INPUT_LIB_INPUT_REPORT_READER_READER_H_ |
| |
| #include <fidl/fuchsia.input.report/cpp/wire.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/stdcompat/inplace_vector.h> |
| #include <lib/trace/event.h> |
| #include <zircon/compiler.h> |
| |
| #include <array> |
| #include <deque> |
| #include <functional> |
| #include <list> |
| #include <memory> |
| #include <mutex> |
| #include <new> |
| #include <optional> |
| |
| #include <fbl/alloc_checker.h> |
| #include <fbl/strong_int.h> |
| |
| namespace input_report_reader { |
| |
| // InputReportReaderManager is used to simplify implementation of input drivers. An input driver may |
| // use InputReportReaderManager to keep track of all upstream readers that want to receive reports. |
| // An upstream driver that wants to read input reports from this device may register with |
| // InputReportReaderManager, which calls CreateReader. When an input report arrives, whether in the |
| // form of HID reports or device readings by polling, etc., the report is pushed to all readers |
| // registered by calling SendReportToAllReaders where it is then translated to |
| // fuchsia_input_report::InputReport. |
| // |
| // If kMaxUnreadReports is non-zero, then at most that many reports are allowed to accumulate for |
| // any client before reports are dropped, starting with the oldest ones first. |
| // |
| // If kMaxBatchSize is greater than 1, then multiple reports may be batched together before being |
| // sent to clients. |
| // |
| // If kMaxBatchSize is greater than 1, then kMaxBatchDelayNs must be set. This is the time in |
| // nanoseconds from receiving the first input report that we will wait before sending a batch to |
| // clients. |
| // |
| // This class creates and manages the InputReportReaders. It is able to send reports |
| // to all existing InputReportReaders. |
| // When this class is destructed, all of the InputReportReaders will be freed. |
| // This class is thread-safe. |
| // Typical Usage: |
| // An InputReport Driver should have one InputReportReaderManager member object. |
| // The Driver should also have some form of InputReport object that can be converted to Fidl. |
| // |
| // Eg: |
| // |
| // class MyTouchScreenDriver { |
| // ... |
| // private: |
| // struct TouchScreenReport { |
| // int64_t x; |
| // int64_t y; |
| // void ToFidlInputReport(fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& |
| // input_report, fidl::AnyArena& allocator) const; |
| // }; |
| // |
| // InputReportReaderManager<TouchScreenReport> input_report_readers_; |
| // }; |
| // |
| // See |
| // https://fuchsia.dev/fuchsia-src/development/drivers/concepts/driver_architectures/input_drivers/input?hl=en |
| template <class Report, size_t kMaxUnreadReports = 0, size_t kMaxBatchSize = 1, |
| zx_duration_t kMaxBatchDelayNs = 0> |
| class InputReportReaderManager final { |
| private: |
| class InputReportReader; |
| class InputReportReaderV2; |
| |
| public: |
| InputReportReaderManager() = default; |
| // This object can't be moved, because InputReportReaders point to this object. |
| InputReportReaderManager(const InputReportReaderManager&) = delete; |
| InputReportReaderManager(InputReportReaderManager&&) = delete; |
| InputReportReaderManager& operator=(const InputReportReaderManager&) = delete; |
| InputReportReaderManager& operator=(InputReportReaderManager&&) = delete; |
| |
| // Create a new InputReportReader that is managed by this InputReportReaderManager. If |
| // initial_report exists, InputReportReaderManager will send initial_report to the new reader. |
| zx_status_t CreateReader(async_dispatcher_t* dispatcher, |
| fidl::ServerEnd<fuchsia_input_report::InputReportsReader> server, |
| std::optional<Report> initial_report = std::nullopt) { |
| ZX_ASSERT(dispatcher); |
| std::scoped_lock lock(lock_); |
| auto reader = |
| std::make_unique<InputReportReader>(this, next_reader_id_, dispatcher, std::move(server)); |
| if (!reader) { |
| return ZX_ERR_INTERNAL; |
| } |
| next_reader_id_++; |
| if (initial_report.has_value()) { |
| reader->ReceiveReport(std::move(*initial_report)); |
| } |
| readers_list_.push_back(std::move(reader)); |
| return ZX_OK; |
| } |
| |
| // Create a new InputReportReaderV2 that is managed by this InputReportReaderManager. If |
| // initial_report exists, InputReportReaderManager will send initial_report to the new reader. |
| // |
| // `dispatcher` must be non-null. |
| // `max_unacknowledged_reports` must be at least 1. |
| zx_status_t CreateReaderV2(async_dispatcher_t* dispatcher, |
| fidl::ServerEnd<fuchsia_input_report::InputReportsReaderV2> server, |
| uint16_t max_unacknowledged_reports, |
| std::optional<Report> initial_report = std::nullopt) { |
| ZX_ASSERT(dispatcher); |
| ZX_ASSERT(max_unacknowledged_reports >= 1); |
| std::scoped_lock lock(lock_); |
| fbl::AllocChecker ac; |
| auto reader = std::unique_ptr<InputReportReaderV2>(new (&ac) InputReportReaderV2( |
| this, next_reader_id_, dispatcher, std::move(server), max_unacknowledged_reports)); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| next_reader_id_++; |
| if (initial_report.has_value()) { |
| reader->ReceiveReport(std::move(*initial_report)); |
| } |
| readers_v2_list_.push_back(std::move(reader)); |
| return ZX_OK; |
| } |
| |
| // Send a report to all InputReportReaders. Returns the total number of reports that are dropped |
| // due to InputReportReader report queues being full. |
| size_t SendReportToAllReaders(const Report& report) { |
| std::scoped_lock lock(lock_); |
| size_t dropped_reports = 0; |
| for (auto& reader : readers_list_) { |
| dropped_reports += reader->ReceiveReport(report); |
| } |
| for (auto& reader : readers_v2_list_) { |
| dropped_reports += reader->ReceiveReport(report); |
| } |
| return dropped_reports; |
| } |
| |
| // Remove a given reader from the list. This is called by the InputReportReader itself |
| // when it wishes to be removed. |
| void RemoveReaderFromList(InputReportReader* reader) { |
| std::scoped_lock lock(lock_); |
| std::erase_if(readers_list_, [reader](const std::unique_ptr<InputReportReader>& item) { |
| return item.get() == reader; |
| }); |
| } |
| |
| void RemoveReaderFromList(InputReportReaderV2* reader) { |
| std::scoped_lock lock(lock_); |
| std::erase_if(readers_v2_list_, [reader](const std::unique_ptr<InputReportReaderV2>& item) { |
| return item.get() == reader; |
| }); |
| } |
| |
| private: |
| static_assert(kMaxBatchSize == 1 || (kMaxBatchSize > 1 && kMaxBatchDelayNs > 0)); |
| static_assert(kMaxUnreadReports == 0 || (kMaxUnreadReports >= kMaxBatchSize)); |
| // Assert that our template type `Report` has the following function: |
| // void ToFidlInputReport(fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& |
| // input_report, fidl::AnyArena& allocator) const; |
| // |
| // TODO(https://fxbug.dev/380355303): Replace the type traits with concepts |
| // when concepts are supported. |
| template <typename T> |
| struct has_to_fidl_input_report { |
| private: |
| template <typename C> |
| static std::true_type test( |
| decltype(static_cast<void (C::*)( |
| fidl::WireTableBuilder<fuchsia_input_report::wire::InputReport>& input_report, |
| fidl::AnyArena& allocator) const>(&C::ToFidlInputReport))); |
| template <typename C> |
| static std::false_type test(...); |
| |
| public: |
| static constexpr bool value = decltype(test<T>(nullptr))::value; |
| }; |
| static_assert( |
| has_to_fidl_input_report<Report>::value, |
| "Report must implement void " |
| "ToFidlInputReport(fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& " |
| "input_report, fidl::AnyArena& allocator) const;"); |
| |
| std::mutex lock_; |
| size_t next_reader_id_ __TA_GUARDED(lock_) = 1; |
| std::list<std::unique_ptr<InputReportReader>> readers_list_ __TA_GUARDED(lock_); |
| std::list<std::unique_ptr<InputReportReaderV2>> readers_v2_list_ __TA_GUARDED(lock_); |
| }; |
| |
| // This class represents an InputReportReader that sends InputReports out to a specific client. |
| // This class is thread safe. |
| // Typical usage: |
| // This class shouldn't be touched directly. An InputReport driver should only manipulate |
| // the InputReportReaderManager. |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| class InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReader final |
| : public fidl::WireServer<fuchsia_input_report::InputReportsReader> { |
| public: |
| // This is only public to make std::unique_ptr work. |
| explicit InputReportReader( |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>* manager, |
| size_t reader_id, async_dispatcher_t* dispatcher, |
| fidl::ServerEnd<fuchsia_input_report::InputReportsReader> server) |
| : dispatcher_(dispatcher), |
| manager_(manager), |
| binding_(dispatcher, std::move(server), this, std::mem_fn(&InputReportReader::OnUnbound)), |
| reader_id_(reader_id) {} |
| |
| size_t ReceiveReport(const Report& report) __TA_EXCLUDES(&report_lock_); |
| |
| void ReadInputReports(ReadInputReportsCompleter::Sync& completer) |
| __TA_EXCLUDES(&report_lock_) override; |
| |
| private: |
| static constexpr size_t kInputReportBufferSize = 4096 * 4; |
| |
| void DelayedReply() __TA_EXCLUDES(&report_lock_); |
| void ReplyWithReports(bool is_delayed = false) __TA_REQUIRES(&report_lock_); |
| void OnUnbound(fidl::UnbindInfo info) { |
| ZX_DEBUG_ASSERT_MSG(manager_, "InputReportReaderManager must outlive InputReportReaders!"); |
| manager_->RemoveReaderFromList(this); |
| } |
| |
| async_dispatcher_t* const dispatcher_; |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>* const |
| manager_; |
| fidl::ServerBinding<fuchsia_input_report::InputReportsReader> binding_; |
| |
| std::mutex report_lock_; |
| std::optional<ReadInputReportsCompleter::Async> completer_ __TA_GUARDED(&report_lock_); |
| fidl::Arena<kInputReportBufferSize> report_allocator_ __TA_GUARDED(report_lock_); |
| std::deque<Report> reports_data_ __TA_GUARDED(report_lock_); |
| async::TaskClosureMethod<InputReportReader, &InputReportReader::DelayedReply> batch_task_ |
| __TA_GUARDED(report_lock_){this}; |
| |
| const size_t reader_id_; |
| }; |
| |
| // Represents an InputReportReaderV2 that pushes InputReports out to a specific client. |
| // Thread safe. |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| class InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReaderV2 final |
| : public fidl::WireServer<fuchsia_input_report::InputReportsReaderV2> { |
| public: |
| // `manager` and `dispatcher` must be non-null and must outlive this reader. |
| // `server` must be valid. |
| // `max_unacknowledged_reports` must be > 0. |
| explicit InputReportReaderV2( |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>* manager, |
| size_t reader_id, async_dispatcher_t* dispatcher, |
| fidl::ServerEnd<fuchsia_input_report::InputReportsReaderV2> server, |
| uint16_t max_unacknowledged_on_input_reports_events) |
| : dispatcher_(dispatcher), |
| manager_(manager), |
| binding_(dispatcher, std::move(server), this, std::mem_fn(&InputReportReaderV2::OnUnbound)), |
| reader_id_(reader_id), |
| max_unacknowledged_on_input_reports_events_(max_unacknowledged_on_input_reports_events) { |
| ZX_DEBUG_ASSERT(manager_ != nullptr); |
| ZX_DEBUG_ASSERT(dispatcher_ != nullptr); |
| ZX_DEBUG_ASSERT(max_unacknowledged_on_input_reports_events_ > 0); |
| } |
| |
| size_t ReceiveReport(const Report& report) __TA_EXCLUDES(&report_lock_); |
| |
| void AcknowledgeReports( |
| fidl::WireServer<fuchsia_input_report::InputReportsReaderV2>::AcknowledgeReportsRequestView |
| request, |
| AcknowledgeReportsCompleter::Sync& completer) __TA_EXCLUDES(&report_lock_) override; |
| |
| void handle_unknown_method( |
| fidl::UnknownMethodMetadata<fuchsia_input_report::InputReportsReaderV2> metadata, |
| fidl::UnknownMethodCompleter::Sync& completer) override { |
| completer.Close(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| private: |
| static constexpr size_t kInputReportBufferSize = 4096 * 4; |
| |
| DEFINE_STRONG_INT(ReportStamp, uint64_t); |
| |
| void DelayedSend() __TA_EXCLUDES(&report_lock_); |
| void SendReports(bool is_delayed = false) __TA_REQUIRES(&report_lock_); |
| void OnUnbound(fidl::UnbindInfo info) { |
| ZX_DEBUG_ASSERT(manager_ != nullptr); |
| manager_->RemoveReaderFromList(this); |
| } |
| |
| async_dispatcher_t* const dispatcher_; |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>* const |
| manager_; |
| fidl::ServerBinding<fuchsia_input_report::InputReportsReaderV2> binding_; |
| |
| std::mutex report_lock_; |
| fidl::Arena<kInputReportBufferSize> report_allocator_ __TA_GUARDED(report_lock_); |
| std::deque<Report> stamped_reports_ __TA_GUARDED(report_lock_); |
| // Stores the report stamps of sent OnInputReports batch events awaiting client acknowledgment. |
| std::deque<ReportStamp> unacknowledged_report_stamps_ __TA_GUARDED(report_lock_); |
| ReportStamp next_report_stamp_ __TA_GUARDED(report_lock_){1}; |
| async::TaskClosureMethod<InputReportReaderV2, &InputReportReaderV2::DelayedSend> batch_task_ |
| __TA_GUARDED(report_lock_){this}; |
| |
| const size_t reader_id_; |
| // Limits maximum unacknowledged OnInputReports batch events in flight per FIDL spec. |
| const uint16_t max_unacknowledged_on_input_reports_events_; |
| }; |
| |
| // Template Implementation. |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline size_t |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReader::ReceiveReport(const Report& report) { |
| std::scoped_lock lock(report_lock_); |
| |
| size_t dropped_reports = 0; |
| if constexpr (kMaxUnreadReports > 0) { |
| // Drop old reports if the client isn't reading them out fast enough. |
| while (reports_data_.size() >= kMaxUnreadReports) { |
| TRACE_INSTANT("input", "InputReportDrop", TRACE_SCOPE_PROCESS); |
| reports_data_.pop_front(); |
| dropped_reports++; |
| } |
| } |
| |
| reports_data_.push_back(report); |
| ReplyWithReports(); |
| return dropped_reports; |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>:: |
| InputReportReader::ReadInputReports(ReadInputReportsCompleter::Sync& completer) { |
| std::scoped_lock lock(report_lock_); |
| if (completer_) { |
| completer.ReplyError(ZX_ERR_ALREADY_BOUND); |
| return; |
| } |
| completer_.emplace(completer.ToAsync()); |
| if (!reports_data_.empty()) { |
| ReplyWithReports(); |
| } |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReader::DelayedReply() { |
| std::scoped_lock lock(report_lock_); |
| ReplyWithReports(/*is_delayed=*/true); |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void |
| InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReader::ReplyWithReports(bool is_delayed) { |
| if (!completer_) { |
| return; |
| } |
| |
| if constexpr (kMaxBatchSize > 1) { |
| if (!is_delayed) { |
| if (reports_data_.size() < kMaxBatchSize) { |
| batch_task_.PostDelayed(dispatcher_, zx::duration(kMaxBatchDelayNs)); |
| return; |
| } |
| if (reports_data_.size() >= kMaxBatchSize) { |
| batch_task_.Cancel(); |
| } |
| } |
| } |
| |
| std::array<fuchsia_input_report::wire::InputReport, |
| fuchsia_input_report::wire::kMaxDeviceReportCount> |
| reports; |
| |
| TRACE_DURATION("input", "InputReportInstance GetReports", "instance_id", reader_id_); |
| size_t num_reports = 0; |
| for (; !reports_data_.empty() && num_reports < reports.size(); num_reports++) { |
| // Build the report. |
| auto input_report = fuchsia_input_report::wire::InputReport::Builder(report_allocator_); |
| |
| // Add some common fields. Will be overwritten if set. |
| input_report.trace_id(TRACE_NONCE()); |
| input_report.event_time(zx_clock_get_monotonic()); |
| |
| reports_data_.front().ToFidlInputReport(input_report, report_allocator_); |
| |
| reports[num_reports] = input_report.Build(); |
| |
| TRACE_FLOW_BEGIN("input", "input_report", reports[num_reports].trace_id()); |
| reports_data_.pop_front(); |
| } |
| |
| completer_->ReplySuccess( |
| fidl::VectorView(fidl::VectorView<fuchsia_input_report::wire::InputReport>::FromExternal( |
| reports.data(), num_reports))); |
| completer_.reset(); |
| |
| if (reports_data_.empty()) { |
| report_allocator_.Reset(); |
| } |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline size_t InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>:: |
| InputReportReaderV2::ReceiveReport(const Report& report) { |
| std::scoped_lock lock(report_lock_); |
| |
| size_t dropped_reports = 0; |
| if constexpr (kMaxUnreadReports > 0) { |
| // Drop old reports if the client isn't reading them out fast enough. |
| while (stamped_reports_.size() >= kMaxUnreadReports) { |
| TRACE_INSTANT("input", "InputReportDrop", TRACE_SCOPE_PROCESS); |
| stamped_reports_.pop_front(); |
| dropped_reports++; |
| } |
| } |
| |
| stamped_reports_.push_back(report); |
| SendReports(); |
| return dropped_reports; |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>:: |
| InputReportReaderV2::AcknowledgeReports( |
| fidl::WireServer<fuchsia_input_report::InputReportsReaderV2>::AcknowledgeReportsRequestView |
| request, |
| AcknowledgeReportsCompleter::Sync& completer) { |
| std::scoped_lock lock(report_lock_); |
| while (!unacknowledged_report_stamps_.empty() && |
| unacknowledged_report_stamps_.front() <= |
| ReportStamp(request->last_acknowledged_report_stamp)) { |
| unacknowledged_report_stamps_.pop_front(); |
| } |
| if (!batch_task_.is_pending()) { |
| SendReports(/*is_running_in_delayed_task=*/true); |
| } else { |
| SendReports(); |
| } |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, |
| kMaxBatchDelayNs>::InputReportReaderV2::DelayedSend() { |
| std::scoped_lock lock(report_lock_); |
| SendReports(/*is_delayed=*/true); |
| } |
| |
| template <class Report, size_t kMaxUnreadReports, size_t kMaxBatchSize, |
| zx_duration_t kMaxBatchDelayNs> |
| inline void InputReportReaderManager<Report, kMaxUnreadReports, kMaxBatchSize, kMaxBatchDelayNs>:: |
| InputReportReaderV2::SendReports(const bool is_running_in_delayed_task) { |
| if constexpr (kMaxBatchSize > 1) { |
| if (!is_running_in_delayed_task) { |
| if (stamped_reports_.size() < kMaxBatchSize) { |
| if (!batch_task_.is_pending()) { |
| batch_task_.PostDelayed(dispatcher_, zx::duration(kMaxBatchDelayNs)); |
| } |
| return; |
| } |
| if (stamped_reports_.size() >= kMaxBatchSize) { |
| batch_task_.Cancel(); |
| } |
| } |
| } |
| |
| while (!stamped_reports_.empty() && |
| unacknowledged_report_stamps_.size() < max_unacknowledged_on_input_reports_events_) { |
| cpp26::inplace_vector<fuchsia_input_report::wire::InputReport, |
| fuchsia_input_report::wire::kMaxDeviceReportCount> |
| reports; |
| |
| TRACE_DURATION("input", "InputReportReaderV2 SendReports", "instance_id", reader_id_); |
| for (; !stamped_reports_.empty() && reports.size() < reports.capacity();) { |
| // Build the report. |
| auto input_report = fuchsia_input_report::wire::InputReport::Builder(report_allocator_); |
| |
| // Add some common fields. Will be overwritten if set. |
| input_report.trace_id(TRACE_NONCE()); |
| input_report.event_time(zx_clock_get_monotonic()); |
| |
| stamped_reports_.front().ToFidlInputReport(input_report, report_allocator_); |
| |
| reports.push_back(input_report.Build()); |
| |
| TRACE_FLOW_BEGIN("input", "input_report", reports.back().trace_id()); |
| stamped_reports_.pop_front(); |
| } |
| |
| if (!reports.empty()) { |
| auto reports_view = fidl::VectorView<fuchsia_input_report::wire::InputReport>::FromExternal( |
| reports.data(), reports.size()); |
| |
| ReportStamp stamp = next_report_stamp_; |
| next_report_stamp_++; |
| |
| fidl::Status status = |
| fidl::WireSendEvent(binding_)->OnInputReports(reports_view, stamp.value()); |
| if (!status.ok()) { |
| report_allocator_.Reset(); |
| break; |
| } |
| unacknowledged_report_stamps_.push_back(stamp); |
| } |
| report_allocator_.Reset(); |
| } |
| } |
| |
| } // namespace input_report_reader |
| |
| #endif // SRC_UI_INPUT_LIB_INPUT_REPORT_READER_READER_H_ |