| // Copyright 2018 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. |
| |
| #include <fuchsia/media/cpp/fidl.h> |
| #include <fuchsia/mediacodec/cpp/fidl.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/closure-queue/closure_queue.h> |
| #include <lib/fidl/cpp/clone.h> |
| #include <lib/fit/defer.h> |
| #include <lib/media/codec_impl/codec_impl.h> |
| #include <lib/media/codec_impl/log.h> |
| #include <threads.h> |
| |
| #include <fbl/macros.h> |
| |
| #include "src/lib/syslog/cpp/logger.h" |
| |
| // "is_bound_checks" - In several lambdas that just send a message, we check |
| // is_bound() first, only because of ZX_POL_BAD_HANDLE ZX_POL_ACTION_EXCEPTION. |
| // If it weren't for that, we really wouldn't care about passing |
| // ZX_HANDLE_INVALID to zx_channel_write(), since the channel error handling is |
| // async (we Unbind(), sweep the in-proc send queue, and only then delete the |
| // Binding). |
| |
| namespace { |
| |
| // The protocol does not permit an unbounded number of in-flight streams, as |
| // that would potentially result in unbounded data queued in the incoming |
| // channel with no valid circuit-breaker value for the incoming channel data. |
| constexpr size_t kMaxInFlightStreams = 10; |
| |
| class ScopedUnlock { |
| public: |
| explicit ScopedUnlock(std::unique_lock<std::mutex>& unique_lock) : unique_lock_(unique_lock) { |
| unique_lock_.unlock(); |
| } |
| ~ScopedUnlock() { unique_lock_.lock(); } |
| |
| private: |
| std::unique_lock<std::mutex>& unique_lock_; |
| ScopedUnlock() = delete; |
| DISALLOW_COPY_ASSIGN_AND_MOVE(ScopedUnlock); |
| }; |
| |
| // Used within ScopedUnlock only. Normally we'd just leave a std::unique_lock |
| // locked until it's destructed. |
| class ScopedRelock { |
| public: |
| explicit ScopedRelock(std::unique_lock<std::mutex>& unique_lock) : unique_lock_(unique_lock) { |
| unique_lock_.lock(); |
| } |
| ~ScopedRelock() { unique_lock_.unlock(); } |
| |
| private: |
| std::unique_lock<std::mutex>& unique_lock_; |
| ScopedRelock() = delete; |
| DISALLOW_COPY_ASSIGN_AND_MOVE(ScopedRelock); |
| }; |
| |
| bool IsStreamErrorRecoverable(fuchsia::media::StreamError e) { |
| using StreamError = fuchsia::media::StreamError; |
| switch (e) { |
| case StreamError::DECRYPTOR_NO_KEY: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| const char* ToString(fuchsia::media::StreamError e) { |
| using StreamError = fuchsia::media::StreamError; |
| switch (e) { |
| case StreamError::UNKNOWN: |
| return "UNKNOWN"; |
| case StreamError::INVALID_INPUT_FORMAT_DETAILS: |
| return "INVALID_INPUT_FORMAT_DETAILS"; |
| case StreamError::INCOMPATIBLE_BUFFERS_PROVIDED: |
| return "INCOMPATIBLE_BUFFERS_PROVIDED"; |
| case StreamError::EOS_PROCESSING: |
| return "EOS_PROCESSING"; |
| case StreamError::DECODER_UNKNOWN: |
| return "DECODER_UNKNOWN"; |
| case StreamError::DECODER_DATA_PARSING: |
| return "DECODER_DATA_PARSING"; |
| case StreamError::ENCODER_UNKNOWN: |
| return "ENCODER_UNKNOWN"; |
| case StreamError::DECRYPTOR_UNKNOWN: |
| return "DECRYPTOR_UNKNOWN"; |
| case StreamError::DECRYPTOR_NO_KEY: |
| return "DECRYPTOR_NO_KEY"; |
| } |
| } |
| |
| const char* GetStreamErrorAdditionalHelpText(fuchsia::media::StreamError e) { |
| using StreamError = fuchsia::media::StreamError; |
| switch (e) { |
| case StreamError::DECRYPTOR_NO_KEY: |
| return "Retry after keys arrive."; |
| default: |
| return ""; |
| } |
| } |
| |
| } // namespace |
| |
| CodecImpl::CodecImpl(fidl::InterfaceHandle<fuchsia::sysmem::Allocator> sysmem, |
| std::unique_ptr<CodecAdmission> codec_admission, |
| async_dispatcher_t* shared_fidl_dispatcher, thrd_t shared_fidl_thread, |
| StreamProcessorParams params, |
| fidl::InterfaceRequest<fuchsia::media::StreamProcessor> request) |
| // The parameters to CodecAdapter constructor here aren't important. |
| : CodecAdapter(lock_, this), |
| codec_admission_(std::move(codec_admission)), |
| shared_fidl_dispatcher_(shared_fidl_dispatcher), |
| shared_fidl_thread_(shared_fidl_thread), |
| shared_fidl_queue_(shared_fidl_dispatcher, shared_fidl_thread), |
| params_(std::move(params)), |
| tmp_sysmem_(std::move(sysmem)), |
| tmp_interface_request_(std::move(request)), |
| binding_(this), |
| stream_control_loop_(&kAsyncLoopConfigNoAttachToCurrentThread) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| ZX_DEBUG_ASSERT(tmp_sysmem_); |
| ZX_DEBUG_ASSERT(tmp_interface_request_); |
| |
| if (codec_admission_) |
| codec_admission_->SetChannelToWaitOn(tmp_interface_request_.channel()); |
| |
| // If the fuchsia::sysmem::Allocator connection dies, so does this CodecImpl. |
| sysmem_.set_error_handler([this](zx_status_t status) { |
| // This handler can't run until after sysmem_ is bound. |
| ZX_DEBUG_ASSERT(was_logically_bound_); |
| this->Fail("CodecImpl sysmem_ channel failed"); |
| }); |
| |
| // This is the binding_'s error handler, not the owner_error_handler_ which |
| // is related but separate. |
| binding_.set_error_handler([this](zx_status_t status) { |
| // This handler can't run until after binding_ is bound. |
| ZX_DEBUG_ASSERT(was_logically_bound_); |
| Unbind(); |
| }); |
| |
| initial_input_format_details_ = IsDecoder() ? &decoder_params().input_details() |
| : IsEncoder() ? &encoder_params().input_details() |
| : &decryptor_params().input_details(); |
| } |
| |
| CodecImpl::~CodecImpl() { |
| // We need ~binding_ to run on fidl_thread() else it's not safe to |
| // un-bind unilaterally. We could potentially relax this if BindAsync() was |
| // never called, but for now we just require this always. |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| |
| if (was_logically_bound_) { |
| // Ensure that StreamControl is told to stop, which also stops InputData by |
| // calling EnsureStreamClosed() as needed. |
| Unbind(); |
| |
| // Wait for StreamControl to be done. |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| // Normally the fidl_thread() waiting for the StreamControl thread to do anything would be |
| // bad, because the fidl_thread() is non-blocking and the StreamControl thread can block on |
| // stuff, but StreamControl thread behavior after was_unbind_started_ = true and |
| // wake_stream_control_condition_.notify_all() does not block and does not wait on |
| // fidl_thread(). So in this case it's ok to wait here. |
| while (!is_stream_control_done_) { |
| stream_control_done_condition_.wait(lock); |
| } |
| } // ~lock |
| |
| EnsureUnbindCompleted(); |
| } |
| |
| // Ensure the CodecAdmission is deleted entirely after ~this, including after any relevant base |
| // class destructors have run. This posted work may only get deleted, not run, since some |
| // environments will Quit() their async::Loop shortly after ~CodecImpl. So to avoid depending on |
| // the destruction order of captures of a lambda, we use a fit::defer which will run it's lambda |
| // when deleted. In this lambda we can force ~CodecAdmission before ~zx::channel, and we know |
| // this lambda will run, whether the lambda further down runs or is just deleted. |
| auto run_when_deleted = fit::defer([codec_admission = std::move(codec_admission_), |
| codec_to_close = std::move(codec_to_close_)]() mutable { |
| // Ensure codec_to_close is destructed only after the codec_admission is destructed. We have |
| // to be fairly explicit about this since the order of lambda members is explicitly |
| // unspecified in C++, so their destruction order is also unspecified. |
| // |
| // We care about the order because a client is fairly likely to immediately retry on seeing |
| // the channel close, and we don't want that to ever bounce off the CodecAdmission for the |
| // instance associated with that same channel. |
| codec_admission = nullptr; |
| |
| // ~codec_to_close (after ~CodecAdmission above). |
| }); |
| // We intentionally don't use shared_fidl_queue_ here. |
| PostSerial(shared_fidl_dispatcher_, [run_when_deleted = std::move(run_when_deleted)] { |
| // ~run_when_deleted will run the lambda above, whether run at the end of this |
| // lambda, or when this lambda is deleted without ever having run during ~async::Loop |
| // or async::Loop::Shutdown(). |
| }); |
| |
| // Before destruction, we know that EnsureBuffersNotConfigured() got called for both input and |
| // output, so we can assert that these are already not set during destruction. |
| ZX_DEBUG_ASSERT(!fake_map_range_[kInputPort]); |
| ZX_DEBUG_ASSERT(!fake_map_range_[kOutputPort]); |
| } |
| |
| std::mutex& CodecImpl::lock() { return lock_; } |
| |
| void CodecImpl::SetCoreCodecAdapter(std::unique_ptr<CodecAdapter> codec_adapter) { |
| ZX_DEBUG_ASSERT(!codec_adapter_); |
| codec_adapter_ = std::move(codec_adapter); |
| } |
| |
| void CodecImpl::BindAsync(fit::closure error_handler) { |
| // While it would potentially be safe to call Bind() from a thread other than |
| // fidl_thread(), we have no reason to permit that. |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // Up to once only. No re-use. |
| ZX_DEBUG_ASSERT(!was_bind_async_called_); |
| ZX_DEBUG_ASSERT(!binding_.is_bound()); |
| ZX_DEBUG_ASSERT(tmp_interface_request_); |
| was_bind_async_called_ = true; |
| |
| zx_status_t start_thread_result = |
| stream_control_loop_.StartThread("StreamControl_loop", &stream_control_thread_); |
| if (start_thread_result != ZX_OK) { |
| // Handle the error async, to be consistent with later errors that must |
| // occur async anyway. Inability to start StreamControl is the only case |
| // where we just allow the owner to "delete this" without using |
| // UnbindLocked(), since UnbindLocked() relies on StreamControl. |
| PostToSharedFidl(std::move(error_handler)); |
| return; |
| } |
| stream_control_queue_.SetDispatcher(stream_control_loop_.dispatcher(), stream_control_thread_); |
| |
| // From here on, we'll only fail the CodecImpl via UnbindLocked(), or by |
| // just calling ~CodecImpl on the FIDL thread. |
| was_logically_bound_ = true; |
| |
| // This doesn't really need to be set until the start of the posted lambda |
| // below, but here is also fine. |
| owner_error_handler_ = std::move(error_handler); |
| |
| // Do most of the bind work on StreamControl async, since CoreCodecInit() |
| // might potentially take a little while longer than makes sense to run on |
| // fidl_thread(). Potential examples: if CoreCodecInit() ends up |
| // essentially evicting some other CodecImpl, or if setting up HW can take a |
| // while, or if getting a scheduling slot on decode HW can require some |
| // waiting, or similar. |
| PostToStreamControl([this] { |
| // This is allowed to take a little while if necessary, using the current |
| // StreamControl thread, which is not shared with any other CodecImpl. |
| CoreCodecInit(*initial_input_format_details_); |
| is_core_codec_init_called_ = true; |
| |
| CoreCodecSetSecureMemoryMode(kOutputPort, PortSecureMemoryMode(kOutputPort)); |
| CoreCodecSetSecureMemoryMode(kInputPort, PortSecureMemoryMode(kInputPort)); |
| |
| if (IsCoreCodecHwBased(kInputPort) || IsCoreCodecHwBased(kOutputPort)) { |
| core_codec_bti_ = CoreCodecBti(); |
| } |
| |
| // We touch FIDL stuff only from the fidl_thread(). While it would |
| // be more efficient to post once to bind and send up to two messages below, |
| // by posting individually we can share more code and have simpler rules for |
| // calling that code. |
| |
| // Once this is posted, we can be dispatching incoming FIDL messages, |
| // concurrent with the rest of the current lambda. Aside from Sync(), most |
| // of that dispatching would tend to land in FailLocked(). The concurrency |
| // is just worth keeping in mind for the rest of the current lambda is all. |
| PostToSharedFidl([this] { |
| zx_status_t status = sysmem_.Bind(std::move(tmp_sysmem_), shared_fidl_dispatcher_); |
| if (status != ZX_OK) { |
| Fail("sysmem_.Bind() failed"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(!tmp_sysmem_); |
| |
| status = binding_.Bind(std::move(tmp_interface_request_), shared_fidl_dispatcher_); |
| if (status != ZX_OK) { |
| Fail("binding_.Bind() failed"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(!tmp_interface_request_); |
| }); |
| |
| input_constraints_ = CoreCodecBuildNewInputConstraints(); |
| |
| ZX_DEBUG_ASSERT(input_constraints_); |
| |
| sent_buffer_constraints_version_ordinal_[kInputPort] = |
| input_constraints_->buffer_constraints_version_ordinal(); |
| PostToSharedFidl([this] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnInputConstraints(fidl::Clone(*input_constraints_)); |
| } |
| }); |
| }); |
| } |
| |
| void CodecImpl::EnableOnStreamFailed() { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| is_on_stream_failed_enabled_ = true; |
| } |
| |
| void CodecImpl::SetInputBufferSettings(fuchsia::media::StreamBufferSettings input_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| PostToStreamControl([this, input_settings = std::move(input_settings)]() mutable { |
| SetInputBufferSettings_StreamControl(std::move(input_settings)); |
| }); |
| } |
| |
| void CodecImpl::SetInputBufferSettings_StreamControl( |
| fuchsia::media::StreamBufferSettings input_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| SetInputBufferSettingsCommon(lock, &input_settings, nullptr); |
| } // ~lock |
| } |
| |
| void CodecImpl::AddInputBuffer(fuchsia::media::StreamBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| PostToStreamControl([this, buffer = std::move(buffer)]() mutable { |
| AddInputBuffer_StreamControl(true, std::move(buffer)); |
| }); |
| } |
| |
| void CodecImpl::AddInputBuffer_StreamControl(bool is_client, fuchsia::media::StreamBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (IsStopping()) { |
| return; |
| } |
| // We must check, because __WARN_UNUSED_RESULT, and it's worth it for the |
| // enforcement and consistency. |
| if (!AddBufferCommon(is_client, kInputPort, std::move(buffer))) { |
| return; |
| } |
| } |
| |
| void CodecImpl::SetInputBufferPartialSettings( |
| fuchsia::media::StreamBufferPartialSettings input_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| PostToStreamControl([this, input_settings = std::move(input_settings)]() mutable { |
| SetInputBufferPartialSettings_StreamControl(std::move(input_settings)); |
| }); |
| } |
| |
| void CodecImpl::SetInputBufferPartialSettings_StreamControl( |
| fuchsia::media::StreamBufferPartialSettings input_partial_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!sysmem_) { |
| FailLocked( |
| "client sent SetInputBufferPartialSettings() to a CodecImpl that " |
| "lacks sysmem_"); |
| return; |
| } |
| SetInputBufferSettingsCommon(lock, nullptr, &input_partial_settings); |
| } // ~lock |
| } |
| |
| void CodecImpl::SetInputBufferSettingsCommon( |
| std::unique_lock<std::mutex>& lock, fuchsia::media::StreamBufferSettings* input_settings, |
| fuchsia::media::StreamBufferPartialSettings* input_partial_settings) { |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (IsStreamActiveLocked()) { |
| FailLocked("client sent SetInputBuffer*Settings() with stream active"); |
| return; |
| } |
| SetBufferSettingsCommon(lock, kInputPort, input_settings, input_partial_settings, |
| *input_constraints_); |
| } |
| |
| void CodecImpl::SetOutputBufferSettings(fuchsia::media::StreamBufferSettings output_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| VLOGF("CodecImpl::SetOutputBufferSettings"); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| SetOutputBufferSettingsCommon(lock, &output_settings, nullptr); |
| } // ~lock |
| } |
| |
| void CodecImpl::SetOutputBufferSettingsCommon( |
| std::unique_lock<std::mutex>& lock, fuchsia::media::StreamBufferSettings* output_settings, |
| fuchsia::media::StreamBufferPartialSettings* output_partial_settings) { |
| if (!output_constraints_) { |
| // invalid client behavior |
| // |
| // client must have received at least the initial OnOutputConstraints() |
| // first before sending SetOutputBufferSettings(). |
| FailLocked( |
| "client sent SetOutputBufferSettings()/SetOutputBufferPartialSettings()" |
| " when no output_constraints_"); |
| return; |
| } |
| |
| // For a mid-stream output format change, this also enforces that the client |
| // can only catch up to the mid-stream format change once. In other words, |
| // if the client has already caught up to the mid-stream config change, the |
| // client no longer has an excuse to re-configure again with a stream |
| // active. |
| // |
| // There's a check in SetBufferSettingsCommonLocked() that ignores this |
| // message if the client's buffer_constraints_version_ordinal is behind |
| // last_required_buffer_constraints_version_ordinal_, which gets updated |
| // under the same lock hold interval as the server's de-configuring of |
| // output buffers. |
| // |
| // There's a check in SetBufferSettingsCommonLocked() that closes the |
| // channel if the client is sending a buffer_constraints_version_ordinal |
| // that's newer than the last sent_buffer_constraints_version_ordinal_. |
| if (IsStreamActiveLocked() && IsOutputConfiguredLocked()) { |
| FailLocked( |
| "client sent SetOutputBufferSettings()/SetOutputBufferPartialSettings()" |
| " with IsStreamActiveLocked() + already-fully-configured output"); |
| return; |
| } |
| |
| SetBufferSettingsCommon(lock, kOutputPort, output_settings, output_partial_settings, |
| output_constraints_->buffer_constraints()); |
| } |
| |
| void CodecImpl::AddOutputBuffer(fuchsia::media::StreamBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| AddOutputBufferInternal(true, std::move(buffer)); |
| } |
| |
| void CodecImpl::AddOutputBufferInternal(bool is_client, fuchsia::media::StreamBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| bool output_buffers_done_configuring = AddBufferCommon(is_client, kOutputPort, std::move(buffer)); |
| if (output_buffers_done_configuring) { |
| // The StreamControl domain _might_ be waiting for output to be configured. |
| wake_stream_control_condition_.notify_all(); |
| } |
| } |
| |
| void CodecImpl::SetOutputBufferPartialSettings( |
| fuchsia::media::StreamBufferPartialSettings output_partial_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| VLOGF("CodecImpl::SetOutputBufferPartialSettings"); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!sysmem_) { |
| FailLocked( |
| "client sent SetOutputBufferPartialSettings() to a CodecImpl " |
| "that lacks a sysmem_"); |
| return; |
| } |
| SetOutputBufferSettingsCommon(lock, nullptr, &output_partial_settings); |
| } // ~lock |
| } |
| |
| void CodecImpl::CompleteOutputBufferPartialSettings(uint64_t buffer_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| |
| if (buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "CompleteOutputBufferPartialSettings client sent even " |
| "buffer_lifetime_ordinal, but must be odd"); |
| return; |
| } |
| |
| if (buffer_lifetime_ordinal != protocol_buffer_lifetime_ordinal_[kOutputPort]) { |
| FailLocked("CompleteOutputBufferPartialSettings bad buffer_lifetime_ordinal"); |
| return; |
| } |
| |
| // If the server is not interested in the client's buffer_lifetime_ordinal, |
| // the client's buffer_lifetime_ordinal won't match the server's |
| // buffer_lifetime_ordinal_. The client will probably later catch up. |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[kOutputPort]) { |
| // The case that ends up here is when a client's output configuration |
| // (whole or last part) is being ignored because it's not yet caught up |
| // with last_required_buffer_constraints_version_ordinal_. |
| |
| // Ignore the client's message. The client will probably catch up later. |
| return; |
| } |
| |
| if (!IsPortBuffersAtLeastPartiallyConfiguredLocked(kOutputPort)) { |
| FailLocked( |
| "CompleteOutputBufferPartialSettings seen without prior " |
| "SetOutputBufferPartialSettings"); |
| return; |
| } |
| |
| if (port_settings_[kOutputPort]->is_complete_seen_output()) { |
| FailLocked( |
| "CompleteOutputBufferPartialSettings permitted exactly once " |
| "after each SetOutputBufferPartialSettings"); |
| return; |
| } |
| |
| // This will cause IsOutputConfiguredLocked() to start returning true. |
| port_settings_[kOutputPort]->SetCompleteSeenOutput(); |
| } // ~lock |
| wake_stream_control_condition_.notify_all(); |
| } |
| |
| void CodecImpl::FlushEndOfStreamAndCloseStream(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamFlushSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| PostToStreamControl([this, stream_lifetime_ordinal] { |
| FlushEndOfStreamAndCloseStream_StreamControl(stream_lifetime_ordinal); |
| }); |
| } |
| |
| void CodecImpl::FlushEndOfStreamAndCloseStream_StreamControl(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| // We re-check some things which were already future-verified a different |
| // way, to allow for flexibility in the future-tracking stuff to permit less |
| // checking in the Output ordering domain (fidl_thread()) without |
| // breaking overall verification of a flush. Any checking in the Output |
| // ordering domain is for the future-tracking's own convenience only. The |
| // checking here is the real checking. |
| |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (!IsStreamActiveLocked() || stream_lifetime_ordinal != stream_lifetime_ordinal_) { |
| // TODO(dustingreen): epitaph |
| FailLocked( |
| "FlushEndOfStreamAndCloseStream() only valid on an active current " |
| "stream (flush does not auto-create a new stream)"); |
| return; |
| } |
| // At this point we know that the stream is not discarded, and not already |
| // flushed previously (because flush will discard the stream as there's |
| // nothing more that the stream is permitted to do). |
| ZX_DEBUG_ASSERT(IsStreamActiveLocked()); |
| ZX_DEBUG_ASSERT(stream_->stream_lifetime_ordinal() == stream_lifetime_ordinal); |
| if (!stream_->input_end_of_stream()) { |
| FailLocked( |
| "FlushEndOfStreamAndCloseStream() is only permitted after " |
| "QueueInputEndOfStream()"); |
| return; |
| } |
| while (!stream_->output_end_of_stream()) { |
| if (stream_->failure_seen()) { |
| return; |
| } |
| // While waiting, we'll continue to send OnOutputPacket(), |
| // OnOutputConstraints(), and continue to process RecycleOutputPacket(), |
| // until the client catches up to the latest config (as needed) and we've |
| // started the send of output end_of_stream packet to the client. |
| // |
| // There is no way for the client to cancel a |
| // FlushEndOfStreamAndCloseStream() short of closing the Codec channel. |
| // Before long, the server will either send the OnOutputEndOfStream(), or |
| // will send OnOmxStreamFailed(), or will close the Codec channel. The |
| // server must do one of those things before long (not allowed to get |
| // stuck while flushing). |
| // |
| // Some core codecs have no way to report mid-stream input data corruption |
| // errors or similar without it being a stream failure, so if there's any |
| // stream error it turns into OnStreamFailed(). It's also permitted for a |
| // server to set error_detected_ bool(s) on output packets and send |
| // OnOutputEndOfStream() despite detected errors, but this is only a |
| // reasonable behavior for the server if the server normally would detect |
| // and report mid-stream input corruption errors without an |
| // OnStreamFailed(). |
| // TODO(fxb/43490): Cancel wait immediately on failure without waiting for |
| // timeout. |
| if (std::cv_status::timeout == |
| output_end_of_stream_seen_.wait_until( |
| lock, std::chrono::system_clock::now() + std::chrono::seconds(5))) { |
| FailLocked("Timeout waiting for end of stream"); |
| break; |
| } |
| } |
| |
| // Now that flush is done, we close the current stream because there is not |
| // any subsequent message for the current stream that's valid. |
| EnsureStreamClosed(lock); |
| } // ~lock |
| } |
| |
| void CodecImpl::CloseCurrentStream(uint64_t stream_lifetime_ordinal, bool release_input_buffers, |
| bool release_output_buffers) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamCloseSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl( |
| [this, stream_lifetime_ordinal, release_input_buffers, release_output_buffers] { |
| CloseCurrentStream_StreamControl(stream_lifetime_ordinal, release_input_buffers, |
| release_output_buffers); |
| }); |
| } |
| |
| void CodecImpl::CloseCurrentStream_StreamControl(uint64_t stream_lifetime_ordinal, |
| bool release_input_buffers, |
| bool release_output_buffers) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| EnsureStreamClosed(lock); |
| if (release_input_buffers) { |
| EnsureBuffersNotConfigured(lock, kInputPort); |
| } |
| if (release_output_buffers) { |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| } |
| } |
| |
| void CodecImpl::Sync(SyncCallback callback) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // By posting to StreamControl ordering domain, we sync both Output ordering |
| // domain (on fidl_thread()) and the StreamControl ordering domain. |
| // |
| // If the posted task doesn't run because stream_control_queue_.StopAndClear() |
| // happened/happens, it doesn't matter because the whole channel will be |
| // closing before long. |
| // |
| // The callback has affinity with fidl_thread(), including the destructor. |
| // This is problematic with respect to the |
| // stream_control_queue_.StopAndClear() called on StreamControl domain during |
| // unbind. Without special handling, that StopAndClear() would try to delete |
| // callback on the StreamControl domain instead of on the fidl_thread(). To |
| // prevent that, we ensure that deletion of the lambda without running the |
| // lambda will still post destruction of callback to fidl_thread(), and this |
| // posting will queue before the lamda that runs |
| // shared_fidl_queue_.StopAndClear(). |
| PostToStreamControl([this, callback_holder = ThreadSafeDeleter<SyncCallback>( |
| &shared_fidl_queue_, std::move(callback))]() mutable { |
| Sync_StreamControl(std::move(callback_holder)); |
| }); |
| } |
| |
| void CodecImpl::Sync_StreamControl(ThreadSafeDeleter<SyncCallback> callback_holder) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (IsStopping()) { |
| // In this case, we rely on ThreadSafeDeleter to delete callback on fidl_thread(). |
| // |
| // The response won't be sent, which is appropriate - the channel is getting closed soon |
| // instead, and the client has to tolerate that. |
| // |
| // ~callback_holder |
| return; |
| } |
| // We post back to FIDL thread to respond to ensure we're not racing with |
| // channel close which could lead to attempting to send to handle value 0 |
| // which can cause process termination. Also, because this fences |
| // BufferAllocation clean close which itself is done async from StreamControl |
| // to FIDL in some cases. |
| PostToSharedFidl([this, callback_holder = std::move(callback_holder)]() mutable { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // call the held callback |
| callback_holder.held()(); |
| }); |
| } |
| |
| void CodecImpl::RecycleOutputPacket(fuchsia::media::PacketHeader available_output_packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| CodecPacket* packet = nullptr; |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!available_output_packet.has_buffer_lifetime_ordinal()) { |
| FailLocked("output packet is missing buffer lifetime ordinal"); |
| return; |
| } |
| |
| if (!CheckOldBufferLifetimeOrdinalLocked(kOutputPort, |
| available_output_packet.buffer_lifetime_ordinal())) { |
| return; |
| } |
| if (available_output_packet.buffer_lifetime_ordinal() < buffer_lifetime_ordinal_[kOutputPort]) { |
| // ignore arbitrarily-stale required by protocol |
| // |
| // Thanks to even values from the client being prohibited, this also |
| // covers mid-stream output config change where the server has already |
| // de-configured output buffers but the client doesn't know about that |
| // yet. We include that case here by setting |
| // buffer_lifetime_ordinal_[kOutputPort] to the next even value |
| // when de-configuring output server-side until the client has |
| // re-configured output. |
| return; |
| } |
| ZX_DEBUG_ASSERT(available_output_packet.buffer_lifetime_ordinal() == |
| buffer_lifetime_ordinal_[kOutputPort]); |
| if (!IsOutputConfiguredLocked()) { |
| FailLocked( |
| "client sent RecycleOutputPacket() for buffer_lifetime_ordinal that " |
| "isn't fully configured yet - bad client behavior"); |
| return; |
| } |
| if (!available_output_packet.has_packet_index()) { |
| FailLocked("output packet is missing packet index"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(IsOutputConfiguredLocked()); |
| if (available_output_packet.packet_index() >= all_packets_[kOutputPort].size()) { |
| FailLocked("out of range packet_index from client in RecycleOutputPacket()"); |
| return; |
| } |
| uint32_t packet_index = available_output_packet.packet_index(); |
| if (all_packets_[kOutputPort][packet_index]->is_free()) { |
| FailLocked( |
| "packet_index already free at protocol level - invalid client " |
| "message"); |
| return; |
| } |
| // Mark free at protocol level. |
| all_packets_[kOutputPort][packet_index]->SetFree(true); |
| |
| // Before handing the packet to the core codec, clear some fields that the |
| // core codec is expected to set (or optionally set in the case of |
| // timestamp_ish). In addition to these parameters, a core codec can emit |
| // output config changes via onCoreCodecMidStreamOutputConstraintsChange(). |
| packet = all_packets_[kOutputPort][packet_index].get(); |
| packet->ClearStartOffset(); |
| packet->ClearValidLengthBytes(); |
| packet->ClearTimestampIsh(); |
| } |
| |
| // Recycle to core codec. |
| CoreCodecRecycleOutputPacket(packet); |
| } |
| |
| void CodecImpl::QueueInputFormatDetails(uint64_t stream_lifetime_ordinal, |
| fuchsia::media::FormatDetails format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| |
| if (!format_details.has_format_details_version_ordinal()) { |
| Fail( |
| "client QueueInputFormatDetails(): Format details have no version " |
| "ordinal."); |
| return; |
| } |
| |
| PostToStreamControl( |
| [this, stream_lifetime_ordinal, format_details = std::move(format_details)]() mutable { |
| QueueInputFormatDetails_StreamControl(stream_lifetime_ordinal, std::move(format_details)); |
| }); |
| } |
| |
| // TODO(dustingreen): Need test coverage for this method, to cover at least |
| // the same format including OOB bytes as were specified during codec creation, |
| // and codec creation with no OOB bytes then this method setting OOB bytes (not |
| // the ideal client usage pattern in the long run since the CreateDecoder() |
| // might decline to provide a optimized but partial Codec implementation, but |
| // should be allowed nonetheless). |
| void CodecImpl::QueueInputFormatDetails_StreamControl( |
| uint64_t stream_lifetime_ordinal, fuchsia::media::FormatDetails format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| |
| if (!CheckWaitEnsureInputConfigured(lock)) { |
| ZX_DEBUG_ASSERT(IsStoppingLocked() || !stream_ || stream_->future_discarded()); |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (stream_lifetime_ordinal > stream_lifetime_ordinal_) { |
| if (!StartNewStream(lock, stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| if (stream_->input_end_of_stream()) { |
| FailLocked("QueueInputFormatDetails() after QueueInputEndOfStream() unexpected"); |
| return; |
| } |
| if (stream_->future_discarded()) { |
| // No reason to handle since the stream is future-discarded. |
| return; |
| } |
| stream_->SetInputFormatDetails( |
| std::make_unique<fuchsia::media::FormatDetails>(std::move(format_details))); |
| // SetOobConfigPending(true) to ensure oob_config_pending() is true. |
| // |
| // This call is needed only to properly handle a call to |
| // QueueInputFormatDetails() mid-stream. For new streams that lack any calls |
| // to QueueInputFormatDetails() before an input packet arrives, the |
| // oob_config_pending() will already be true because it starts true for a new |
| // stream. For QueueInputFormatDetails() at the start of a stream before any |
| // packets, oob_config_pending() will already be true. |
| // |
| // For decoders this is basically a pending oob_bytes. For encoders |
| // this pending config change can potentially include uncompressed format |
| // details, if mid-stream format change is supported by the encoder. |
| stream_->SetOobConfigPending(true); |
| } |
| |
| void CodecImpl::QueueInputPacket(fuchsia::media::Packet packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!packet.has_stream_lifetime_ordinal()) { |
| FailLocked( |
| "client QueueInputPacket() with packet that has no stream lifetime " |
| "ordinal"); |
| return; |
| } |
| if (!EnsureFutureStreamSeenLocked(packet.stream_lifetime_ordinal())) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, packet = std::move(packet)]() mutable { |
| QueueInputPacket_StreamControl(std::move(packet)); |
| }); |
| } |
| |
| void CodecImpl::QueueInputPacket_StreamControl(fuchsia::media::Packet packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| ZX_DEBUG_ASSERT(packet.has_stream_lifetime_ordinal()); |
| |
| if (!packet.has_header()) { |
| Fail("client QueueInputPacket() with packet has no header"); |
| return; |
| } |
| fuchsia::media::PacketHeader temp_header_copy = fidl::Clone(packet.header()); |
| |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| // Unless we cancel this cleanup, we'll free the input packet back to the |
| // client. |
| auto send_free_input_packet_locked = |
| fit::defer([this, header = std::move(temp_header_copy)]() mutable { |
| // Mute sending this if FailLocked() was called previously, in case |
| // the reason we're here is something horribly wrong with the packet |
| // header. This way we avoid repeating gibberish back to the client. |
| // While that gibberish might be a slight clue for debugging in some |
| // cases, it's not valid protocol, so don't send it. If |
| // IsStoppingLocked(), the Codec channel will close soon, making this |
| // response unnecessary. |
| if (!IsStoppingLocked()) { |
| SendFreeInputPacketLocked(std::move(header)); |
| } |
| }); |
| |
| if (!packet.header().has_buffer_lifetime_ordinal()) { |
| FailLocked( |
| "client QueueInputPacket() with header that has no buffer lifetime " |
| "ordinal"); |
| return; |
| } |
| if (!CheckOldBufferLifetimeOrdinalLocked(kInputPort, |
| packet.header().buffer_lifetime_ordinal())) { |
| return; |
| } |
| |
| if (!packet.has_stream_lifetime_ordinal()) { |
| FailLocked("client QueueInputPacket() without packet stream_lifetime_ordinal."); |
| return; |
| } |
| if (!CheckStreamLifetimeOrdinalLocked(packet.stream_lifetime_ordinal())) { |
| return; |
| } |
| |
| if (!CheckWaitEnsureInputConfigured(lock)) { |
| ZX_DEBUG_ASSERT(IsStoppingLocked() || (stream_ && stream_->future_discarded())); |
| return; |
| } |
| |
| // For input, mid-stream config changes are not a thing and input buffers |
| // are never unilaterally de-configured by the Codec server. |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kInputPort] == |
| port_settings_[kInputPort]->buffer_lifetime_ordinal()); |
| |
| // For this message we're strict re. buffer_lifetime_ordinal. |
| // |
| // In contrast to output, the server doesn't use even values to track config |
| // changes that the client doesn't know about yet, since the server can't |
| // unilaterally demand any changes to the input settings after initially |
| // specifying the input constraints. |
| // |
| // One could somewhat-convincingly argue that this field in this particular |
| // message is a bit pointless, but it might serve to detect client-side |
| // bugs faster thanks to this check. |
| if (packet.header().buffer_lifetime_ordinal() != |
| port_settings_[kInputPort]->buffer_lifetime_ordinal()) { |
| FailLocked("client QueueInputPacket() with invalid buffer_lifetime_ordinal."); |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(packet.stream_lifetime_ordinal() >= stream_lifetime_ordinal_); |
| |
| if (packet.stream_lifetime_ordinal() > stream_lifetime_ordinal_) { |
| // This case implicitly starts a new stream. If the client wanted to |
| // ensure that the old stream would be fully processed, the client would |
| // have sent FlushEndOfStreamAndCloseStream() previously, whose |
| // processing (previous to reaching here) takes care of the flush. |
| // |
| // Start a new stream, synchronously. |
| if (!StartNewStream(lock, packet.stream_lifetime_ordinal())) { |
| return; |
| } |
| } |
| ZX_DEBUG_ASSERT(packet.stream_lifetime_ordinal() == stream_lifetime_ordinal_); |
| |
| if (!packet.header().has_packet_index()) { |
| FailLocked("client QueueInputPacket() with packet has no packet index"); |
| return; |
| } |
| if (packet.header().packet_index() >= all_packets_[kInputPort].size()) { |
| FailLocked( |
| "client QueueInputPacket() with packet_index out of range - " |
| "packet_index: %u size: %u", |
| packet.header().packet_index(), all_packets_[kInputPort].size()); |
| return; |
| } |
| if (!packet.has_buffer_index()) { |
| FailLocked("client QueueInputPacket() with packet has no buffer index"); |
| return; |
| } |
| if (packet.buffer_index() >= all_buffers_[kInputPort].size()) { |
| FailLocked("client QueueInputPacket() with buffer_index out of range"); |
| return; |
| } |
| |
| // Protocol check re. free/busy coherency. This applies to packets only, |
| // not buffers. |
| if (!all_packets_[kInputPort][packet.header().packet_index()]->is_free()) { |
| FailLocked("client QueueInputPacket() with packet_index !free"); |
| return; |
| } |
| |
| if (stream_->input_end_of_stream()) { |
| FailLocked("QueueInputPacket() after QueueInputEndOfStream() unexpeted"); |
| return; |
| } |
| |
| if (stream_->future_discarded()) { |
| // Don't queue to core codec. The stream_ may have never fully started, |
| // or may have been future-discarded since. Either way, skip queueing to |
| // the core codec. |
| // |
| // If the stream didn't fully start - as in, the client moved on to |
| // another stream before fully configuring output, then the core codec is |
| // not presently in a state compatible with queueing input, but the Codec |
| // interface is. So in that case, we must avoid queueing to the core |
| // codec for correctness. |
| // |
| // If the stream was just future-discarded after fully starting, then this |
| // is just an optimization to avoid giving the core codec more work to do |
| // for a stream the client has already discarded. |
| // |
| // ~send_free_input_packet_locked |
| // ~lock |
| return; |
| } |
| |
| all_packets_[kInputPort][packet.header().packet_index()]->SetFree(false); |
| |
| // Sending OnFreeInputPacket() will happen later instead, when the core |
| // codec gives back the packet. |
| send_free_input_packet_locked.cancel(); |
| } // ~lock |
| |
| if (stream_->oob_config_pending()) { |
| HandlePendingInputFormatDetails(); |
| stream_->SetOobConfigPending(false); |
| } |
| |
| CodecPacket* core_codec_packet = all_packets_[kInputPort][packet.header().packet_index()].get(); |
| core_codec_packet->SetBuffer(all_buffers_[kInputPort][packet.buffer_index()].get()); |
| if (!packet.has_start_offset()) { |
| Fail("client QueueInputPacket() with packet has no start offset"); |
| return; |
| } |
| core_codec_packet->SetStartOffset(packet.start_offset()); |
| if (!packet.has_valid_length_bytes()) { |
| Fail("client QueueInputPacket() with packet has no valid length bytes"); |
| return; |
| } |
| core_codec_packet->SetValidLengthBytes(packet.valid_length_bytes()); |
| if (packet.has_timestamp_ish()) { |
| core_codec_packet->SetTimstampIsh(packet.timestamp_ish()); |
| } else { |
| core_codec_packet->ClearTimestampIsh(); |
| } |
| |
| if (core_codec_packet->valid_length_bytes() <= 0) { |
| Fail("client QueueInputPacket() with valid_length_bytes 0 - not allowed"); |
| return; |
| } |
| if (core_codec_packet->start_offset() + core_codec_packet->valid_length_bytes() < |
| core_codec_packet->start_offset()) { |
| Fail("client QueueInputPacket() start_offset + valid_length_bytes overflow"); |
| return; |
| } |
| if (core_codec_packet->start_offset() + core_codec_packet->valid_length_bytes() > |
| core_codec_packet->buffer()->size()) { |
| Fail("client QueueInputPacket() with packet end > buffer size"); |
| return; |
| } |
| |
| // Flush the data out to RAM if needed. |
| if (IsCoreCodecHwBased(kInputPort) && |
| port_settings_[kInputPort]->coherency_domain() == fuchsia::sysmem::CoherencyDomain::CPU) { |
| // This flushes only the portion of the buffer that the packet is |
| // referencing. |
| zx_status_t status = core_codec_packet->CacheFlush(); |
| if (status != ZX_OK) { |
| Fail("CacheFlush() failed"); |
| return; |
| } |
| } |
| |
| // We don't need to be under lock for this, because the fact that we're on the |
| // StreamControl domain is enough to guarantee that any other control of the |
| // core codec will occur after this. |
| CoreCodecQueueInputPacket(core_codec_packet); |
| } |
| |
| void CodecImpl::QueueInputEndOfStream(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, stream_lifetime_ordinal] { |
| QueueInputEndOfStream_StreamControl(stream_lifetime_ordinal); |
| }); |
| } |
| |
| void CodecImpl::QueueInputEndOfStream_StreamControl(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| |
| if (!CheckWaitEnsureInputConfigured(lock)) { |
| ZX_DEBUG_ASSERT(IsStoppingLocked() || (stream_ && stream_->future_discarded())); |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (stream_lifetime_ordinal > stream_lifetime_ordinal_) { |
| // We start a new stream given an end-of-stream for a stream we've not |
| // seen before, since allowing empty streams to not be errors may be nicer |
| // to use. |
| if (!StartNewStream(lock, stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| |
| if (stream_->input_end_of_stream()) { |
| FailLocked("client already sent QueueInputEndOfStream() for this stream"); |
| return; |
| } |
| stream_->SetInputEndOfStream(); |
| |
| if (stream_->future_discarded()) { |
| // Don't queue to core codec. The stream_ may have never fully started, |
| // or may have been future-discarded since. Either way, skip queueing to |
| // core codec. We only really must do this because the stream may not have |
| // ever fully started, in the case where the client moves on to a new |
| // stream before catching up to latest output config. |
| return; |
| } |
| } // ~lock |
| |
| CoreCodecQueueInputEndOfStream(); |
| } |
| |
| zx_status_t CodecImpl::Pin(uint32_t options, const zx::vmo& vmo, uint64_t offset, uint64_t size, |
| zx_paddr_t* addrs, size_t addrs_count, zx::pmt* pmt) { |
| ZX_DEBUG_ASSERT(*core_codec_bti_); |
| return core_codec_bti_->pin(options, vmo, offset, size, addrs, addrs_count, pmt); |
| } |
| |
| bool CodecImpl::CheckWaitEnsureInputConfigured(std::unique_lock<std::mutex>& lock) { |
| // Ensure/finish input configuration. |
| if (!IsPortBuffersAtLeastPartiallyConfiguredLocked(kInputPort)) { |
| FailLocked( |
| "client QueueInput*() with input buffers not at least partially " |
| "configured"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kInputPort] % 2 == 1); |
| // The client is required to know that sysmem is in fact done allocating the |
| // BufferCollection successfully before the client sends |
| // QueueInput...StreamControl. We can't trust a client to necessarily get |
| // that right however, so rather than just getting stuck indefinitely in that |
| // case, we detect by asking sysmem to verify that it has allocated the |
| // BufferCollection successfully. This verification happens async, but will |
| // shortly cause WaitEnsureSysmemReadyOnInput() to return and |
| // IsStoppingLocked() to return true if verification fails. |
| if (!IsInputConfiguredLocked()) { |
| PostToSharedFidl([this, buffer_lifetime_ordinal = buffer_lifetime_ordinal_[kInputPort]] { |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[kInputPort]) { |
| // stale; no problem; old buffers were allocated fine and client already |
| // moved on after that. |
| return; |
| } |
| // Else previous buffer_lifetime_ordinal check would have noticed. |
| ZX_DEBUG_ASSERT(port_settings_[kInputPort]); |
| // paranoid check - assert above believed to be valid |
| if (!port_settings_[kInputPort]) { |
| return; |
| } |
| // Else IsStoppingLocked() check above would have returned. |
| ZX_DEBUG_ASSERT(port_settings_[kInputPort]->buffer_collection().is_bound()); |
| // paranoid check - assert above believed to be valid |
| if (!port_settings_[kInputPort]->buffer_collection().is_bound()) { |
| return; |
| } |
| port_settings_[kInputPort]->buffer_collection()->CheckBuffersAllocated( |
| [this, buffer_lifetime_ordinal](zx_status_t status) { |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[kInputPort]) { |
| // stale; no problem; old buffers were allocated fine and client |
| // already moved on after that. |
| return; |
| } |
| if (status != ZX_OK) { |
| // This will cause any in-progress WaitEnsureSysmemReadyOnInput() |
| // to return shortly and IsStoppingLocked() will be true. |
| FailLocked( |
| "Probably client did QueueInput* before the client " |
| "determined that sysmem was done successfully allocating " |
| "buffers after most recent SetInputBufferPartialSettings()"); |
| return; |
| } |
| }); |
| }); |
| if (!WaitEnsureSysmemReadyOnInput(lock)) { |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return false; |
| } |
| } |
| if (!IsInputConfiguredLocked()) { |
| FailLocked("client QueueInput*() with input buffers not configured"); |
| return false; |
| } |
| return true; |
| } |
| |
| void CodecImpl::UnbindLocked() { |
| // We must have first gotten far enough through BindAsync() before calling |
| // UnbindLocked(). |
| ZX_DEBUG_ASSERT(was_logically_bound_); |
| |
| if (was_unbind_started_) { |
| // Ignore the second trigger if we have a near-simultaneous failure from |
| // StreamControl thread (for example) and from fidl_thread() (for |
| // example). The first will start unbinding, and the second will be |
| // ignored. Since completion of the Unbind() call doesn't imply anything |
| // about how done the unbind is, there's no need for the second caller to |
| // be blocked waiting for the first caller's unbind to be done. |
| return; |
| } |
| |
| if (codec_admission_) |
| codec_admission_->SetCodecIsClosing(); |
| |
| // Tell StreamControl to not start any more work. |
| was_unbind_started_ = true; |
| wake_stream_control_condition_.notify_all(); |
| |
| // Unbind() / UnbindLocked() can be called from any thread. |
| // |
| // Regardless of what thread UnbindLocked() is called on, "this" will remain |
| // allocated at least until the caller of UnbindLocked() releases lock_. |
| // |
| // In all cases, this posted lambda runs after BindAsync()'s work that's |
| // posted to StreamControl, because any/all calls to UnbindLocked() happen |
| // after BindAsync() has posted to StreamControl. |
| // |
| // We know the stream_control_queue_ isn't stopped yet, because the present |
| // method is idempotent and the lambda being posted just below has the only |
| // call to stream_control_queue_.StopAndClear(). |
| ZX_DEBUG_ASSERT(!stream_control_queue_.is_stopped()); |
| PostToStreamControl([this] { |
| // At this point we know that no more streams will be started by |
| // StreamControl ordering domain (thanks to was_unbind_started_ / |
| // IsStoppingLocked() checks), but lambdas posted to the StreamControl |
| // ordering domain (by the fidl_thread() or by core codec) may still |
| // be creating other activity such as posting lambdas to StreamControl or |
| // fidl_thread(). |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| // Stop core codec associated with this CodecImpl, partly to make sure it |
| // stops running code that could make calls into this CodecImpl, and |
| // partly to ensure the core codec isn't in the middle of anything when it |
| // gets deleted. |
| // |
| // We know the core codec won't start more activity because the core codec |
| // isn't allowed to initiate actions while there's no active stream, and |
| // because no new active stream will be created. All _StreamControl |
| // methods check IsStoppingLocked() at the start, and the StreamControl |
| // ordering domain is the only domain that ever starts a stream. |
| // |
| // We intentionally don't check for IsStoppingLocked() in protocol |
| // dispatch methods running on fidl_thread(). For example the codec |
| // must tolerate calls to configure buffers after EnsureStreamClosed() |
| // here. The Unbind() later is what silences the protocol message |
| // dispatch methods. Checking for IsStoppingLocked() in protocol dispatch |
| // methods would only decrease the probability of certain event orderings, |
| // not eliminate those orderings, so it's actually better to let them |
| // happen to get more coverage of those orderings. |
| if (is_core_codec_init_called_) { |
| EnsureStreamClosed(lock); |
| EnsureBuffersNotConfigured(lock, kInputPort); |
| } |
| |
| // Because the current path is the only path that sets this bool to true, |
| // and the current path is run-once. |
| ZX_DEBUG_ASSERT(!is_stream_control_done_); |
| // Because stream_control_done_ is false, and ~CodecImpl waits for |
| // is_stream_control_done_ true before shared_fidl_queue_.StopAndClear(). |
| ZX_DEBUG_ASSERT(!shared_fidl_queue_.is_stopped()); |
| |
| // We do this from here so we know that this thread won't run any more |
| // tasks after the currently-running task. |
| // |
| // The currently-running StreamControl task (this method) still gets to |
| // run to completion. |
| // |
| // TODO(dustingreen): We probably could lean more heavily on this Quit() |
| // and do less checking of IsStoppingLocked() in StreamControl tasks. |
| // This TODO is not meant to imply that all current checking of |
| // IsStoppingLocked() is ok to remove (less, not none). |
| stream_control_loop_.Quit(); |
| |
| // This deletes any further tasks already queued to StreamControl, and will immediately |
| // delete any additional tasks that try to queue to StreamControl. We also need to ensure the |
| // first time stream_control_queue_.StopAndClear() runs is on stream_control_thread_, per |
| // ClosureQueue's usage rules. |
| stream_control_queue_.StopAndClear(); |
| |
| // We're ready to let EnsureUnbindCompleted() and ~CodecImpl do the rest. |
| // |
| // The core codec has been stopped, so it has no current stream. The core codec is required |
| // to be delete-able when it has no current stream, and required not to asynchronously post |
| // more work to the CodecImpl (because calling onCoreCodec... methods is not allowed when |
| // there is no current stream). |
| // |
| // The binding_.Unbind() will run during EnsureUnbindCompleted() on the FIDL thread, so no |
| // more FIDL dispatching to this CodecImpl after that. |
| // |
| // The stream_control_loop_.JoinThreads() will run during ~CodecImpl, so no more activity from |
| // stream_control_thread_ after that. |
| // |
| // Anything posted using PostToSharedFidl() can be deleted instead of run since the whole |
| // CodecImpl is going away, and shared_fidl_queue_ makes it safe for ~CodecImpl to complete |
| // without needing to wait/fence past previously-posted labmdas to FIDL thread. |
| is_stream_control_done_ = true; |
| // Must notify_all() under lock_ in this case since ~CodecImpl can run as soon as |
| // stream_control_done_ = true just above. |
| stream_control_done_condition_.notify_all(); |
| |
| // If we're not running from ~CodecImpl, we need to run the owner_error_handler_ on the FIDL |
| // thread, which will in turn call ~CodecImpl. If we are running from ~CodecImpl, then we're |
| // already on the FIDL thread, and this posted work won't run thanks to shared_fidl_queue_ |
| // just deleting the posted task instead, in which case the owner_error_handler_ just gets |
| // deleted instead of running (the usual semantics in response to unsolicited destruction). |
| // |
| // Must post under lock_ in this case else ~CodecImpl can have already finished as soon as |
| // stream_control_done_ = true above. |
| PostToSharedFidl([this, client_error_handler = std::move(owner_error_handler_)] { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // We go ahead and finish up the un-binding aspects (because we can free up |
| // resources prior to the client code potentially running ~CodecImpl async |
| // later). |
| // |
| // However, this doesn't finish up aspects related to ordering release of |
| // resources before acquisition of new resources. In particular, this call |
| // unbinds the channel, but intentionally doesn't close the channel itself until |
| // after ~CodecImpl and after ~CodecAdmission. The intent is to prevent the |
| // possibility that overly-agressive client retries on channel closure by the |
| // server could build up many CodecImpl instances, even if different instances |
| // happen to use different FIDL threads (also potentially different than FIDL |
| // thread on which a new CodecAdmission is created). By only closing the channel |
| // itself as the last thing after all other cleanup is fully done, we don't |
| // trigger the client to create a new CodecImpl while the old one still exists. |
| EnsureUnbindCompleted(); |
| // This call is expected to run ~CodecImpl, either synchronously during this |
| // call or shortly later async. |
| client_error_handler(); |
| }); |
| } // ~lock |
| |
| // "this" will be deleted shortly async when lambda posted just above runs, or we're returning |
| // back to rest of ~CodecImpl, or ~CodecImpl is racing/running separately and completing |
| // immediately after ~lock just above. Regardless, done here. |
| return; |
| }); |
| // "this" remains allocated until caller releases lock_. |
| } |
| |
| void CodecImpl::Unbind() { |
| std::lock_guard<std::mutex> lock(lock_); |
| UnbindLocked(); |
| // ~lock |
| // |
| // "this" may be deleted very shortly after ~lock, depending on what thread |
| // Unbind() is called from. |
| } |
| |
| void CodecImpl::EnsureUnbindCompleted() { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| ZX_DEBUG_ASSERT(was_logically_bound_); |
| if (was_unbind_completed_) { |
| return; |
| } |
| // Or will be, before this method returns. |
| was_unbind_completed_ = true; |
| |
| // Unbind from the channel so we won't see any more incoming FIDL messages. This binding doesn't |
| // own "this". |
| // |
| // The Unbind() stops any additional FIDL dispatching re. this CodecImpl. |
| if (binding_.is_bound()) { |
| codec_to_close_ = binding_.Unbind().TakeChannel(); |
| } |
| |
| // This isn't strictly necessary, but since we can potentially delete a queued |
| // task here (before a client-called ~CodecImpl), we go ahead and do that now. |
| // |
| // This is partly a very minor potential resource deletion, and partly so we |
| // get a nicer stack if anything should go wrong during that deletion; partly |
| // so we get a nicer stack if somehow the JoinThreads() gets stuck (it |
| // shouldn't since Quit() already happened). |
| stream_control_loop_.JoinThreads(); |
| stream_control_loop_.Shutdown(); |
| |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| |
| // By this point both PortSettings should have already been deleted. |
| ZX_DEBUG_ASSERT(!port_settings_[kInputPort]); |
| ZX_DEBUG_ASSERT(!port_settings_[kOutputPort]); |
| |
| // Unbind the sysmem_ fuchsia::sysmem::Allocator connection - this also |
| // ensures that any in-flight requests' completions will not run. |
| sysmem_.Unbind(); |
| } // ~lock |
| |
| // Any previously-posted tasks via shared_fidl_queue_ are deleted here without running. |
| // |
| // If we're shutting down because UnbindLocked() was run first upon discovery of an |
| // internally-noticed error, then previously-queued sending of FIDL messages on the FIDL thread |
| // already ran before the EnsureUnbindCompleted(), which was posted after the sends. |
| // |
| // If we're running ~CodecImpl because the client code is just deleting CodecImpl for whatever |
| // client-initiated reason, then previously queueud sending of FIDL messages can be just deleted |
| // here without the sends actually occurring, which is fine since in that case the client code |
| // has no particular expectation that any particular messages were sent before deletion vs. not |
| // getting sent due to deletion. |
| shared_fidl_queue_.StopAndClear(); |
| } |
| |
| fuchsia::mediacodec::SecureMemoryMode CodecImpl::OutputSecureMemoryMode() { |
| if (!IsDecoder() && !IsDecryptor()) { |
| return fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| if (IsDecoder()) { |
| if (!decoder_params().has_secure_output_mode()) { |
| return fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| return decoder_params().secure_output_mode(); |
| } else { |
| ZX_DEBUG_ASSERT(IsDecryptor()); |
| if (!decryptor_params().has_require_secure_mode()) { |
| return fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| return decryptor_params().require_secure_mode() ? fuchsia::mediacodec::SecureMemoryMode::ON |
| : fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| } |
| |
| fuchsia::mediacodec::SecureMemoryMode CodecImpl::InputSecureMemoryMode() { |
| if (!IsDecoder()) { |
| return fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| if (!decoder_params().has_secure_input_mode()) { |
| return fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| return decoder_params().secure_input_mode(); |
| } |
| |
| fuchsia::mediacodec::SecureMemoryMode CodecImpl::PortSecureMemoryMode(CodecPort port) { |
| if (port == kOutputPort) { |
| return OutputSecureMemoryMode(); |
| } else { |
| ZX_DEBUG_ASSERT(port == kInputPort); |
| return InputSecureMemoryMode(); |
| } |
| } |
| |
| bool CodecImpl::IsPortSecureRequired(CodecPort port) { |
| // Return false for DYNAMIC, if/when we add that. |
| return PortSecureMemoryMode(port) == fuchsia::mediacodec::SecureMemoryMode::ON; |
| } |
| |
| bool CodecImpl::IsPortSecurePermitted(CodecPort port) { |
| // Return true for DYNAMIC, if/when we add that. |
| return PortSecureMemoryMode(port) != fuchsia::mediacodec::SecureMemoryMode::OFF; |
| } |
| |
| bool CodecImpl::IsStreamActiveLocked() { |
| ZX_DEBUG_ASSERT(!!stream_ == (stream_lifetime_ordinal_ % 2 == 1)); |
| return !!stream_; |
| } |
| |
| void CodecImpl::SetBufferSettingsCommon( |
| std::unique_lock<std::mutex>& lock, CodecPort port, |
| fuchsia::media::StreamBufferSettings* settings, |
| fuchsia::media::StreamBufferPartialSettings* partial_settings, |
| const fuchsia::media::StreamBufferConstraints& stream_constraints) { |
| ZX_DEBUG_ASSERT(port == kInputPort && thrd_current() == stream_control_thread_ || |
| port == kOutputPort && thrd_current() == fidl_thread()); |
| ZX_DEBUG_ASSERT(!IsStoppingLocked()); |
| |
| // Invariant |
| // |
| // Either we've never seen settings, or the logical buffer_lifetime_ordinal_ |
| // is either the last accepted from the client or one more than that as a way |
| // of cleanly permitting the server to unilaterally de-configure output |
| // buffers. |
| if (settings) { |
| if (!settings->has_buffer_lifetime_ordinal()) { |
| FailLocked("settings missing buffer lifetime ordinal"); |
| return; |
| } |
| if (!settings->has_buffer_constraints_version_ordinal()) { |
| FailLocked("settings missing buffer constraints version ordinal"); |
| return; |
| } |
| if (!settings->has_packet_count_for_server()) { |
| FailLocked("settings missing packet_count_for_server"); |
| return; |
| } |
| if (!settings->has_packet_count_for_client()) { |
| FailLocked("settings missing packet_count_for_client"); |
| return; |
| } |
| if (!settings->has_per_packet_buffer_bytes()) { |
| FailLocked("settings missing per_packet_buffer_bytes"); |
| return; |
| } |
| } else { |
| if (!partial_settings->has_buffer_lifetime_ordinal()) { |
| FailLocked("partial_settings do not have buffer lifetime ordinal"); |
| return; |
| } |
| if (!partial_settings->has_buffer_constraints_version_ordinal()) { |
| FailLocked("partial_settings do not have buffer constraints version ordinal"); |
| return; |
| } |
| if (!partial_settings->has_packet_count_for_server()) { |
| FailLocked("partial_settings missing packet_count_for_server"); |
| return; |
| } |
| if (!partial_settings->has_packet_count_for_client()) { |
| FailLocked("partial_settings missing packet_count_for_client"); |
| return; |
| } |
| if (!partial_settings->has_sysmem_token() || !partial_settings->sysmem_token().is_valid()) { |
| FailLocked("partial_settings missing valid sysmem_token"); |
| return; |
| } |
| } |
| |
| ZX_DEBUG_ASSERT( |
| !port_settings_[port] || |
| (buffer_lifetime_ordinal_[port] >= port_settings_[port]->buffer_lifetime_ordinal() && |
| buffer_lifetime_ordinal_[port] <= port_settings_[port]->buffer_lifetime_ordinal() + 1)); |
| |
| // The caller may be providing StreamBufferSettings or |
| // StreamBufferPartialSettings, but not both. |
| ZX_DEBUG_ASSERT(!!settings ^ !!partial_settings); |
| |
| // Extract buffer_lifetime_ordinal and buffer_constraints_version_ordinal from |
| // whichever of StreamBufferSettings or StreamBufferPartialSettings the caller |
| // is providing. |
| uint64_t buffer_lifetime_ordinal = |
| settings ? settings->buffer_lifetime_ordinal() : partial_settings->buffer_lifetime_ordinal(); |
| |
| uint64_t buffer_constraints_version_ordinal = |
| settings ? settings->buffer_constraints_version_ordinal() |
| : partial_settings->buffer_constraints_version_ordinal(); |
| |
| if (buffer_lifetime_ordinal <= protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "buffer_lifetime_ordinal <= " |
| "protocol_buffer_lifetime_ordinal_[port] - port: %d", |
| port); |
| return; |
| } |
| if (buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "Only odd values for buffer_lifetime_ordinal are permitted - port: %d " |
| "value %lu", |
| port, buffer_lifetime_ordinal); |
| return; |
| } |
| protocol_buffer_lifetime_ordinal_[port] = buffer_lifetime_ordinal; |
| |
| if (buffer_constraints_version_ordinal > sent_buffer_constraints_version_ordinal_[port]) { |
| FailLocked("Client sent too-new buffer_constraints_version_ordinal - port: %d", port); |
| return; |
| } |
| |
| if (buffer_constraints_version_ordinal < |
| last_required_buffer_constraints_version_ordinal_[port]) { |
| // ignore - client will probably catch up later |
| return; |
| } |
| |
| // We've peeled off too new and too old above. |
| ZX_DEBUG_ASSERT(buffer_constraints_version_ordinal >= |
| last_required_buffer_constraints_version_ordinal_[port] && |
| buffer_constraints_version_ordinal <= |
| sent_buffer_constraints_version_ordinal_[port]); |
| |
| // We've already checked above that the buffer_lifetime_ordinal is in |
| // sequence. |
| ZX_DEBUG_ASSERT(!port_settings_[port] || |
| buffer_lifetime_ordinal > buffer_lifetime_ordinal_[port]); |
| |
| if (settings) { |
| if (!ValidateBufferSettingsVsConstraintsLocked(port, *settings, stream_constraints)) { |
| // This assert is safe only because this thread still holds lock_. This |
| // is asserting that ValidateBufferSettingsVsConstraintsLocked() already |
| // called FailLocked(). |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return; |
| } |
| } else { |
| if (!ValidatePartialBufferSettingsVsConstraintsLocked(port, *partial_settings, |
| stream_constraints)) { |
| // This assert is safe only because this thread still holds lock_. This |
| // is asserting that ValidateBufferSettingsVsConstraintsLocked() already |
| // called FailLocked(). |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return; |
| } |
| } |
| |
| // Little if any reason to do this outside the lock. |
| EnsureBuffersNotConfigured(lock, port); |
| |
| // This also starts the new buffer_lifetime_ordinal. |
| { // scope port_settings, to enforce not using it after we've moved it out |
| std::unique_ptr<PortSettings> port_settings; |
| if (settings) { |
| port_settings = std::make_unique<PortSettings>(this, port, std::move(*settings)); |
| } else { |
| port_settings = std::make_unique<PortSettings>(this, port, std::move(*partial_settings)); |
| } |
| port_settings_[port] = std::move(port_settings); |
| } // ~port_settings, which has been moved out, so we can't use it anyway |
| buffer_lifetime_ordinal_[port] = port_settings_[port]->buffer_lifetime_ordinal(); |
| |
| // If partial_settings_param, the client won't be doing any AddInputBuffer() |
| // or AddOutputBuffer(). Instead, CodecImpl uses the token in |
| // partial_settings to get a fuchsia.sysmem.BufferCollection view, |
| // SetConstraints() on that BufferCollection, and then get allocated buffers |
| // from sysmem directly. |
| |
| if (port_settings_[port]->is_partial_settings()) { |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token = |
| port_settings_[port]->TakeToken(); |
| // We intentionally don't want to hand the sysmem token directly to the core |
| // codec, at least for now (maybe later it'll be necessary). |
| ZX_DEBUG_ASSERT(!port_settings_[port]->partial_settings().has_sysmem_token()); |
| fuchsia::sysmem::BufferCollectionConstraints buffer_collection_constraints = |
| [this, port, &lock, &stream_constraints]() { |
| // port_settings_[port] can only change on this thread so are safe to |
| // read outside the lock. |
| ScopedUnlock unlock(lock); |
| return CoreCodecGetBufferCollectionConstraints(port, stream_constraints, |
| port_settings_[port]->partial_settings()); |
| }(); |
| // The core codec doesn't fill out usage directly. Instead we fill it out |
| // here. |
| if (!FixupBufferCollectionConstraintsLocked(port, stream_constraints, |
| port_settings_[port]->partial_settings(), |
| &buffer_collection_constraints)) { |
| // FixupBufferCollectionConstraints() already called Fail(). |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return; |
| } |
| // For output, the only reason we re-post here is to share the lock |
| // acquisition code with input. |
| PostToSharedFidl( |
| [this, port, buffer_lifetime_ordinal = buffer_lifetime_ordinal_[port], |
| token = std::move(token), |
| buffer_collection_constraints = std::move(buffer_collection_constraints)]() mutable { |
| std::lock_guard<std::mutex> lock(lock_); |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[port]) { |
| return; |
| } |
| if (!sysmem_) { |
| return; |
| } |
| if (IsStoppingLocked()) { |
| return; |
| } |
| sysmem_->BindSharedCollection( |
| std::move(token), |
| port_settings_[port]->NewBufferCollectionRequest(shared_fidl_dispatcher_)); |
| port_settings_[port]->buffer_collection().set_error_handler( |
| [this, port, buffer_lifetime_ordinal](zx_status_t status) { |
| std::lock_guard<std::mutex> lock(lock_); |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[port]) { |
| // It's fine if a BufferCollection fails after we're already |
| // done using it. |
| return; |
| } |
| // We're intentionally picky about the BufferCollection failing |
| // too soon, as all clean closes should use Close(), which will |
| // avoid causing this. If we find a case where a client |
| // legitimately needs to try one way then if that fails try |
| // another way, we should see if we can avoid the need to do |
| // that by expressing in sysmem constraints, or more likely just |
| // accept that such a client will need to start with a new codec |
| // instance for the 2nd try. |
| UnbindLocked(); |
| }); |
| port_settings_[port]->buffer_collection()->SetConstraints( |
| true, std::move(buffer_collection_constraints)); |
| port_settings_[port]->buffer_collection()->WaitForBuffersAllocated( |
| [this, port, buffer_lifetime_ordinal]( |
| zx_status_t status, |
| fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) mutable { |
| OnBufferCollectionInfo(port, buffer_lifetime_ordinal, status, |
| std::move(buffer_collection_info)); |
| }); |
| }); |
| } else { |
| // This path probably won't stick around for very long, and involves a |
| // substantial amount of simulation of the new way based on old settings. To |
| // the degree that it's more indirect than usual, it's to try and |
| // use/simulate the new way as much as possible despite the client using the |
| // old way. |
| |
| const fuchsia::media::StreamBufferSettings& settings = port_settings_[port]->settings(); |
| |
| // If !port_settings_[port]->is_partial_settings(), we'll simulate the new |
| // way based on the old-style settings. This way we can get the core codec |
| // to fill out image_format_constraints, and can inform the core codec of |
| // BufferCollectionInfo_2, even if we're not actually using sysmem (in this |
| // path for now). |
| fuchsia::media::StreamBufferPartialSettings fake_partial_settings; |
| fake_partial_settings.set_buffer_lifetime_ordinal(settings.buffer_lifetime_ordinal()); |
| fake_partial_settings.set_buffer_constraints_version_ordinal( |
| settings.buffer_constraints_version_ordinal()); |
| fake_partial_settings.set_packet_count_for_server(settings.packet_count_for_server()); |
| fake_partial_settings.set_packet_count_for_client(settings.packet_count_for_client()); |
| fake_partial_settings.set_single_buffer_mode(settings.has_single_buffer_mode() && |
| settings.single_buffer_mode()); |
| ZX_DEBUG_ASSERT(!fake_partial_settings.has_sysmem_token()); |
| |
| fuchsia::sysmem::BufferCollectionConstraints buffer_collection_constraints = |
| [this, port, &lock, &stream_constraints, &fake_partial_settings] { |
| ScopedUnlock unlock(lock); |
| return CoreCodecGetBufferCollectionConstraints(port, stream_constraints, |
| fake_partial_settings); |
| }(); |
| |
| // The core codec doesn't fill out usage directly. Instead we fill it out |
| // here. |
| if (!FixupBufferCollectionConstraintsLocked(port, stream_constraints, fake_partial_settings, |
| &buffer_collection_constraints)) { |
| // FixupBufferCollectionConstraints() already called Fail(). |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return; |
| } |
| |
| fuchsia::sysmem::BufferCollectionInfo_2 fake; |
| fake.buffer_count = fake_partial_settings.single_buffer_mode() |
| ? 1 |
| : fake_partial_settings.packet_count_for_server() + |
| fake_partial_settings.packet_count_for_client(); |
| fake.settings.buffer_settings.size_bytes = |
| fake_partial_settings.single_buffer_mode() |
| ? settings.per_packet_buffer_bytes() * fake.buffer_count |
| : settings.per_packet_buffer_bytes(); |
| fake.settings.buffer_settings.is_physically_contiguous = |
| buffer_collection_constraints.has_buffer_memory_constraints && |
| buffer_collection_constraints.buffer_memory_constraints.physically_contiguous_required; |
| // The client should use sysmem (and SetInputBufferPartialSettings / |
| // SetOutputBufferPartialSettings) if secure is required. |
| fake.settings.buffer_settings.is_secure = false; |
| if (buffer_collection_constraints.image_format_constraints_count != 0) { |
| fake.settings.has_image_format_constraints = true; |
| // pick option 0 arbitrarily (essentially picking PixelFormat 0 among |
| // possibly multiple PixelFormat(s)) |
| fake.settings.image_format_constraints = |
| fidl::Clone(buffer_collection_constraints.image_format_constraints[0]); |
| } else { |
| fake.settings.has_image_format_constraints = false; |
| } |
| CoreCodecSetBufferCollectionInfo(port, fake); |
| } |
| } |
| |
| void CodecImpl::OnBufferCollectionInfo( |
| CodecPort port, uint64_t buffer_lifetime_ordinal, zx_status_t status, |
| fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| |
| if (port == kInputPort) { |
| PostSysmemCompletion([this, port, buffer_lifetime_ordinal, status, |
| buffer_collection_info = std::move(buffer_collection_info)]() mutable { |
| OnBufferCollectionInfoInternal(port, buffer_lifetime_ordinal, status, |
| std::move(buffer_collection_info)); |
| }); |
| } else { |
| ZX_DEBUG_ASSERT(port == kOutputPort); |
| OnBufferCollectionInfoInternal(port, buffer_lifetime_ordinal, status, |
| std::move(buffer_collection_info)); |
| } |
| } |
| |
| void CodecImpl::OnBufferCollectionInfoInternal( |
| CodecPort port, uint64_t buffer_lifetime_ordinal, zx_status_t allocate_status, |
| fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) { |
| ZX_DEBUG_ASSERT(port == kInputPort && thrd_current() == stream_control_thread_ || |
| port == kOutputPort && thrd_current() == fidl_thread()); |
| |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| // The buffer_lifetime_ordinal_[port] can only change on the current thread. |
| if (buffer_lifetime_ordinal != buffer_lifetime_ordinal_[port]) { |
| // stale response |
| return; |
| } |
| if (allocate_status != ZX_OK) { |
| FailLocked( |
| "OnBufferCollectionInfoLocked() sees failure - port: %d " |
| "allocate_status: %d", |
| port, allocate_status); |
| return; |
| } |
| } // ~lock |
| |
| uint32_t buffer_count = buffer_collection_info.buffer_count; |
| |
| // This code trusts sysmem to really be sysmem and to behave correctly, but |
| // doesn't hurt to double-check some things in debug build. |
| ZX_DEBUG_ASSERT(buffer_count >= 1); |
| ZX_DEBUG_ASSERT(buffer_count <= buffer_collection_info.buffers.size()); |
| // Spot check that the boundary between valid and invalid handles is where it |
| // should be. |
| ZX_DEBUG_ASSERT(buffer_collection_info.buffers[buffer_count - 1].vmo.is_valid()); |
| ZX_DEBUG_ASSERT(buffer_count == buffer_collection_info.buffers.size() || |
| !buffer_collection_info.buffers[buffer_count].vmo.is_valid()); |
| |
| // Let's move the VMO handles out first, so that the BufferCollectionInfo_2 we |
| // send to the core codec doesn't have the VMO handles. We want the core |
| // codec to get its VMO handles via the CodecBuffer*(s) we'll provide shortly |
| // below. |
| zx::vmo vmos[buffer_collection_info.buffers.size()]; |
| for (uint32_t i = 0; i < buffer_count; ++i) { |
| vmos[i] = std::move(buffer_collection_info.buffers[i].vmo); |
| ZX_DEBUG_ASSERT(!buffer_collection_info.buffers[i].vmo.is_valid()); |
| } |
| |
| // Now we can tell the core codec about the collection info. The core codec |
| // can clone the FIDL struct if it wants, or can just copy out any info it |
| // wants from specific fields. |
| CoreCodecSetBufferCollectionInfo(port, buffer_collection_info); |
| |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal == buffer_lifetime_ordinal_[port]); |
| |
| // The only way port_settings_[port] gets cleared is if |
| // buffer_lifetime_ordinal changes. |
| ZX_DEBUG_ASSERT(port_settings_[port]); |
| |
| // This completes the settings, analogous to having completed |
| // SetInputBufferSettings()/SetOutputBufferSettings(). |
| port_settings_[port]->SetBufferCollectionInfo(std::move(buffer_collection_info)); |
| } |
| |
| if (IsPortSecureRequired(port) && !port_settings_[port]->is_secure()) { |
| Fail("IsPortSecureRequired(port) && !port_settings_[port]->is_secure() - port: %d", port); |
| return; |
| } |
| if (!IsPortSecurePermitted(port) && port_settings_[port]->is_secure()) { |
| Fail("!IsPortSecurePermitted(port) && port_settings_[port]->is_secure() - port: %d", port); |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(!fake_map_range_[port]); |
| if (port_settings_[port]->is_secure()) { |
| if (IsCoreCodecMappedBufferUseful(port)) { |
| zx_status_t status = |
| FakeMapRange::Create(port_settings_[port]->vmo_usable_size(), &fake_map_range_[port]); |
| if (status != ZX_OK) { |
| Fail("FakeMapRange::Init() failed"); |
| return; |
| } |
| } |
| } |
| |
| // We convert the buffer_collection_info into AddInputBuffer_StreamControl() |
| // and AddOutputBufferInternal() calls, almost as if the client were adding |
| // the buffers itself (but without the check that the client isn't adding |
| // buffers itself while using sysmem). |
| for (uint32_t i = 0; i < buffer_count; i++) { |
| // While under the lock we'll move out the stuff we need into codec_buffer. |
| fuchsia::media::StreamBuffer stream_buffer; |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal == buffer_lifetime_ordinal_[port]); |
| ZX_DEBUG_ASSERT(port_settings_[port]); |
| |
| stream_buffer.set_buffer_lifetime_ordinal(buffer_lifetime_ordinal); |
| stream_buffer.set_buffer_index(i); |
| fuchsia::media::StreamBufferDataVmo data_vmo; |
| data_vmo.set_vmo_handle(std::move(vmos[i])); |
| data_vmo.set_vmo_usable_start(port_settings_[port]->vmo_usable_start(i)); |
| data_vmo.set_vmo_usable_size(port_settings_[port]->vmo_usable_size()); |
| fuchsia::media::StreamBufferData data; |
| data.set_vmo(std::move(data_vmo)); |
| stream_buffer.set_data(std::move(data)); |
| } // ~lock |
| if (port == kInputPort) { |
| AddInputBuffer_StreamControl(false, std::move(stream_buffer)); |
| } else { |
| ZX_DEBUG_ASSERT(port == kOutputPort); |
| AddOutputBufferInternal(false, std::move(stream_buffer)); |
| } |
| } |
| } |
| |
| void CodecImpl::EnsureBuffersNotConfigured(std::unique_lock<std::mutex>& lock, CodecPort port) { |
| // This method can be called on input only if there's no current stream. |
| // |
| // On output, this method can be called if there's no current stream or if |
| // we're in the middle of an output config change. |
| // |
| // On input, this can only be called on stream_control_thread_. |
| // |
| // On output, this can be called on stream_control_thread_ or fidl_thread(). |
| ZX_DEBUG_ASSERT(port == kInputPort && thrd_current() == stream_control_thread_ || |
| port == kOutputPort && (thrd_current() == stream_control_thread_ || |
| thrd_current() == fidl_thread())); |
| |
| is_port_buffers_configured_[port] = false; |
| if (buffer_lifetime_ordinal_[port] % 2 == 1) { |
| buffer_lifetime_ordinal_[port]++; |
| } |
| if (port_settings_[port]) { |
| // This will close the BufferCollection (async as-needed) cleanly, without causing the |
| // LogicalBufferCollection to fail. Mainly we care so we can more easily tell during debugging |
| // whether a LogicalBufferCollection was cleanly closed by all participants, vs. potentially |
| // getting failed by a participant exiting or non-cleanly closing. A Sync() by the client is |
| // sufficient to ensure this async close is done. |
| port_settings_[port] = nullptr; |
| } |
| |
| // Ensure that buffers aren't with the core codec. |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecEnsureBuffersNotConfigured(port); |
| } // ~unlock |
| |
| // For mid-stream output config change, the caller is responsible for ensuring |
| // that buffers are not with the HW first. |
| // |
| // TODO(dustingreen): Check anything relevant to buffers not presently being |
| // with the HW. |
| // ZX_DEBUG_ASSERT(all_packets_[port].empty() || |
| // !all_packets_[port][0]->is_with_hw()); |
| |
| // This ~FakeMapRange (which calls zx::vmar::destroy()) is among the motivations for calling |
| // EnsureBuffersNotConfigured() during the Unbind() sequence / during ~CodecImpl. |
| if (fake_map_range_[port]) { |
| fake_map_range_[port].reset(); |
| } |
| |
| all_packets_[port].clear(); |
| all_buffers_[port].clear(); |
| ZX_DEBUG_ASSERT(all_packets_[port].empty()); |
| ZX_DEBUG_ASSERT(all_buffers_[port].empty()); |
| } |
| |
| bool CodecImpl::ValidateBufferSettingsVsConstraintsLocked( |
| CodecPort port, const fuchsia::media::StreamBufferSettings& settings, |
| const fuchsia::media::StreamBufferConstraints& constraints) { |
| if (!settings.has_packet_count_for_server()) { |
| FailLocked("settings do not have packet count for server"); |
| return false; |
| } |
| |
| if (settings.packet_count_for_server() < constraints.packet_count_for_server_min()) { |
| FailLocked("packet_count_for_server < packet_count_for_server_min"); |
| return false; |
| } |
| |
| if (settings.packet_count_for_server() > constraints.packet_count_for_server_max()) { |
| FailLocked("packet_count_for_server > packet_count_for_server_max"); |
| return false; |
| } |
| if (!settings.has_packet_count_for_client()) { |
| FailLocked("settings do not have packet count for client"); |
| return false; |
| } |
| if (settings.packet_count_for_client() < constraints.packet_count_for_client_min()) { |
| FailLocked( |
| "packet_count_for_client < packet_count_for_client_min - port: %d %d < " |
| "%d", |
| port, settings.packet_count_for_client(), constraints.packet_count_for_client_min()); |
| return false; |
| } |
| if (settings.packet_count_for_client() > constraints.packet_count_for_client_max()) { |
| FailLocked("packet_count_for_client > packet_count_for_client_max"); |
| return false; |
| } |
| |
| if (!settings.has_per_packet_buffer_bytes()) { |
| FailLocked("settings do not have per packet buffer bytes"); |
| return false; |
| } |
| |
| if (settings.per_packet_buffer_bytes() < constraints.per_packet_buffer_bytes_min()) { |
| FailLocked("per_packet_buffer_bytes < per_packet_buffer_bytes_min"); |
| return false; |
| } |
| if (settings.per_packet_buffer_bytes() > constraints.per_packet_buffer_bytes_max()) { |
| FailLocked("per_packet_buffer_bytes > per_packet_buffer_bytes_max"); |
| return false; |
| } |
| |
| if ((settings.has_single_buffer_mode() && settings.single_buffer_mode()) && |
| (!constraints.has_single_buffer_mode_allowed() || |
| !constraints.single_buffer_mode_allowed())) { |
| FailLocked("single_buffer_mode && !single_buffer_mode_allowed"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::ValidatePartialBufferSettingsVsConstraintsLocked( |
| CodecPort port, const fuchsia::media::StreamBufferPartialSettings& partial_settings, |
| const fuchsia::media::StreamBufferConstraints& constraints) { |
| // Most of the constraints will be handled by telling sysmem about them, not |
| // via the client, so there's not a ton to validate here. |
| if ((partial_settings.has_single_buffer_mode() && partial_settings.single_buffer_mode()) && |
| !constraints.single_buffer_mode_allowed()) { |
| FailLocked("single_buffer_mode && !single_buffer_mode_allowed"); |
| return false; |
| } |
| bool packet_count_needed = |
| partial_settings.has_single_buffer_mode() && partial_settings.single_buffer_mode(); |
| ZX_DEBUG_ASSERT(partial_settings.sysmem_token().is_valid()); |
| if (packet_count_needed) { |
| if (!partial_settings.has_packet_count_for_server()) { |
| FailLocked("missing packet_count_for_server"); |
| return false; |
| } |
| if (!partial_settings.has_packet_count_for_client()) { |
| FailLocked("missing packet_count_for_client"); |
| return false; |
| } |
| } else { |
| // If packet count isn't required, it can still be provided, but if it's |
| // provided, it should be provided in two parts like usual. |
| if (partial_settings.has_packet_count_for_server() != |
| partial_settings.has_packet_count_for_client()) { |
| FailLocked("has_packet_count_for_server != has_packet_count_for_client"); |
| return false; |
| } |
| } |
| // if needed or provided anyway |
| if (partial_settings.has_packet_count_for_server()) { |
| if (partial_settings.packet_count_for_server() < constraints.packet_count_for_server_min()) { |
| FailLocked("packet_count_for_server < packet_count_for_server_min"); |
| return false; |
| } |
| if (partial_settings.packet_count_for_server() > constraints.packet_count_for_server_max()) { |
| FailLocked("packet_count_for_server > packet_count_for_server_max"); |
| return false; |
| } |
| } |
| // if needed or provided anyway |
| if (partial_settings.has_packet_count_for_client()) { |
| if (partial_settings.packet_count_for_client() < constraints.packet_count_for_client_min()) { |
| FailLocked("packet_count_for_client > packet_count_for_client_max"); |
| return false; |
| } |
| if (partial_settings.packet_count_for_client() > constraints.packet_count_for_client_max()) { |
| FailLocked("packet_count_for_client > packet_count_for_client_max"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool CodecImpl::AddBufferCommon(bool is_client, CodecPort port, |
| fuchsia::media::StreamBuffer buffer) { |
| ZX_DEBUG_ASSERT(port == kInputPort && (thrd_current() == stream_control_thread_) || |
| port == kOutputPort && (thrd_current() == fidl_thread())); |
| bool buffers_done_configuring = false; |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| |
| if (!buffer.has_buffer_lifetime_ordinal()) { |
| FailLocked("Client send a buffer without a buffer_lifetime_ordinal"); |
| return false; |
| } |
| |
| if (buffer.buffer_lifetime_ordinal() % 2 == 0) { |
| FailLocked( |
| "Client sent even buffer_lifetime_ordinal, but must be odd - exiting " |
| "- port: %u\n", |
| port); |
| return false; |
| } |
| |
| if (buffer.buffer_lifetime_ordinal() != protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "Incoherent SetOutputBufferSettings()/SetInputBufferSettings() + " |
| "AddOutputBuffer()/AddInputBuffer()s - exiting - port: %d\n", |
| port); |
| return false; |
| } |
| |
| // If the server is not interested in the client's buffer_lifetime_ordinal, |
| // the client's buffer_lifetime_ordinal won't match the server's |
| // buffer_lifetime_ordinal_. The client will probably later catch up. |
| if (buffer.buffer_lifetime_ordinal() != buffer_lifetime_ordinal_[port]) { |
| // The case that ends up here is when a client's output configuration |
| // (whole or last part) is being ignored because it's not yet caught up |
| // with last_required_buffer_constraints_version_ordinal_. |
| |
| // This case won't happen for input, at least for now. This is an assert |
| // rather than a client behavior check, because previous client protocol |
| // checks have already peeled off any invalid client behavior that might |
| // otherwise cause this assert to trigger. |
| ZX_DEBUG_ASSERT(port == kOutputPort); |
| |
| // Ignore the client's message. The client will probably catch up later. |
| return false; |
| } |
| |
| if (is_client && port_settings_[port]->is_partial_settings()) { |
| FailLocked("AddInputBuffer()/AddOutputBuffer() not permitted when using sysmem"); |
| return false; |
| } |
| if (!buffer.has_buffer_index()) { |
| FailLocked("client sent buffer without index"); |
| return false; |
| } |
| if (buffer.buffer_index() != all_buffers_[port].size()) { |
| FailLocked( |
| "AddOutputBuffer()/AddInputBuffer() had buffer_index out of sequence " |
| "- port: %d buffer_index: %u all_buffers_[port].size(): %lu", |
| port, buffer.buffer_index(), all_buffers_[port].size()); |
| return false; |
| } |
| |
| uint32_t required_buffer_count = port_settings_[port]->buffer_count(); |
| if (buffer.buffer_index() >= required_buffer_count) { |
| FailLocked("AddOutputBuffer()/AddInputBuffer() extra buffer - port: %d", port); |
| return false; |
| } |
| |
| std::unique_ptr<CodecBuffer> local_buffer = std::unique_ptr<CodecBuffer>( |
| new CodecBuffer(this, port, std::move(buffer), port_settings_[port]->is_secure())); |
| if (IsCoreCodecMappedBufferUseful(port)) { |
| if (fake_map_range_[port]) { |
| // The fake_map_range_[port]->base() is % ZX_PAGE_SIZE == 0, which is the same as a mapping |
| // would be. There are sufficient virtual pages starting at FakeMapRange::base() to permit |
| // CodecBuffer to include the low-order vmo_usable_start % ZX_PAGE_SIZE bits in |
| // CodecBuffer::base(), for any vmo_usable_start() value (even the worst case of |
| // ZX_PAGE_SIZE - 1, and buffer size % ZX_PAGE_SIZE == 2). By including those low-order |
| // intra-page-offset bits, we can treat non-secure and secure cases similarly. |
| local_buffer->FakeMap(fake_map_range_[port]->base()); |
| } else { |
| // So far, there's little reason to avoid doing the Map() part under the |
| // lock, even if it can be a bit more time consuming, since there's no data |
| // processing happening at this point anyway, and there wouldn't be any |
| // happening in any other code location where we could potentially move the |
| // Map() either. |
| if (!local_buffer->Map()) { |
| FailLocked("AddOutputBuffer()/AddInputBuffer() couldn't Map() new buffer - port: %d", |
| port); |
| return false; |
| } |
| } |
| } |
| |
| // We keep the buffers pinned for DMA continuously, since there's not much benefit to un-pinning |
| // and re-pinning them (so far). By pinning, we prevent sysmem from recycling the |
| // BufferCollection VMOs until the driver has re-started and un-quarantined pinned pages (via |
| // its BTI), after ensuring the HW is no longer doing DMA from/to the pages. |
| // |
| // TODO(38650): All CodecAdapter(s) that start memory access that can continue beyond VMO |
| // handle closure during process death/termination should have a BTI. Resolving this TODO will |
| // require updating at least the amlogic-video VP9 decoder to provide a BTI. |
| // |
| // TODO(38651): Currently OEMCrypto's indirect (via FIDL) SMC calls that take physical addresses |
| // are not guaranteed to be fully over/done before VMO handles are auto-closed by OEMCrypto |
| // assuming OEMCryto's process dies/terminates. |
| if (IsCoreCodecHwBased(port) && *core_codec_bti_) { |
| zx_status_t status = local_buffer->Pin(); |
| if (status != ZX_OK) { |
| FailLocked("buffer->Pin() failed - status: %d port: %d", status, port); |
| return false; |
| } |
| } |
| |
| { |
| ScopedUnlock unlock(lock); |
| // Inform the core codec up-front about each buffer. |
| CoreCodecAddBuffer(port, local_buffer.get()); |
| } |
| all_buffers_[port].push_back(std::move(local_buffer)); |
| if (all_buffers_[port].size() == required_buffer_count) { |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[port] == |
| port_settings_[port]->buffer_lifetime_ordinal()); |
| // Stash this while we can, before the client de-configures. |
| last_provided_buffer_constraints_version_ordinal_[port] = |
| port_settings_[port]->buffer_constraints_version_ordinal(); |
| // Now we allocate all_packets_[port]. |
| ZX_DEBUG_ASSERT(all_packets_[port].empty()); |
| uint32_t packet_count = port_settings_[port]->packet_count(); |
| for (uint32_t i = 0; i < packet_count; i++) { |
| // Private constructor to prevent core codec maybe creating its own |
| // Packet instances (which isn't the intent) seems worth the hassle of |
| // not using make_unique<>() here. |
| all_packets_[port].push_back(std::unique_ptr<CodecPacket>( |
| new CodecPacket(port_settings_[port]->buffer_lifetime_ordinal(), i))); |
| } |
| |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| |
| // A core codec can take action here to finish configuring buffers if |
| // it's able, or can delay configuring buffers until |
| // CoreCodecStartStream() or |
| // CoreCodecMidStreamOutputBufferReConfigFinish() if that works better |
| // for the core codec. |
| // |
| // In any case, during a mid-stream output constraints change, the core |
| // codec must not call any onCoreCodecOutput* methods until the core |
| // codec sees CoreCodecStopStream() (after stopping the stream, in |
| // preparation for the next stream), or |
| // CoreCodecMidStreamOutputBufferReConfigFinish(). |
| // |
| // In other words, this call does /not/ imply un-pausing output. |
| CoreCodecConfigureBuffers(port, all_packets_[port]); |
| |
| // All output packets need to start with the core codec. This is |
| // implicit for the Codec interface (implied by adding the last output |
| // buffer) but explicit in the CodecAdapter interface. |
| if (port == kOutputPort) { |
| for (uint32_t i = 0; i < packet_count; i++) { |
| CoreCodecRecycleOutputPacket(all_packets_[kOutputPort][i].get()); |
| } |
| } |
| } // ~unlock |
| |
| is_port_buffers_configured_[port] = true; |
| buffers_done_configuring = true; |
| |
| // For client-called AddOutputBuffer(), the last buffer being added is |
| // analogous to CompleteOutputBufferPartialSettings(); we handle that |
| // analogous-ness in IsOutputConfiguredLocked() (not by pretending we got |
| // a CompleteOutputBufferPartialSettings() here), so |
| // is_port_buffers_configured_[port] = true above is enough to make |
| // IsOutputConfiguredLocked() return true if this is a client-driven |
| // AddOutputBuffer(). |
| } |
| } |
| return buffers_done_configuring; |
| } |
| |
| bool CodecImpl::CheckOldBufferLifetimeOrdinalLocked(CodecPort port, |
| uint64_t buffer_lifetime_ordinal) { |
| // The client must only send odd values. 0 is even so we don't need a |
| // separate check for that. |
| if (buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "CheckOldBufferLifetimeOrdinalLocked() - buffer_lifetime_ordinal must " |
| "be odd"); |
| return false; |
| } |
| if (buffer_lifetime_ordinal > protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "client sent new buffer_lifetime_ordinal in message type that doesn't " |
| "allow new buffer_lifetime_ordinals"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::CheckStreamLifetimeOrdinalLocked(uint64_t stream_lifetime_ordinal) { |
| if (stream_lifetime_ordinal % 2 != 1) { |
| FailLocked("stream_lifetime_ordinal must be odd.\n"); |
| return false; |
| } |
| if (stream_lifetime_ordinal < stream_lifetime_ordinal_) { |
| FailLocked("client sent stream_lifetime_ordinal that went backwards"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::StartNewStream(std::unique_lock<std::mutex>& lock, |
| uint64_t stream_lifetime_ordinal) { |
| VLOGF("StartNewStream()"); |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| ZX_DEBUG_ASSERT((stream_lifetime_ordinal % 2 == 1) && "new stream_lifetime_ordinal must be odd"); |
| |
| if (IsStoppingLocked()) { |
| // Don't start a new stream if the whole CodecImpl is already stopping. |
| // |
| // A completely different path will take care of calling |
| // EnsureStreamClosed() during CodecImpl stop. |
| // |
| // TODO(dustingreen): If all callers are already checking this at the top |
| // of each relevant .*_StreamControl method, then we don't necessarily need |
| // this check, but consider any intervals where lock_ isn't held also - we |
| // don't want the wait for stream_control_thread_ to exit to ever be long |
| // when stopping this CodecImpl. |
| return false; |
| } |
| |
| EnsureStreamClosed(lock); |
| ZX_DEBUG_ASSERT(!IsStreamActiveLocked()); |
| |
| // Now it's time to start the new stream. We start the new stream at |
| // Codec layer first then core codec layer. |
| |
| if (!IsInputConfiguredLocked()) { |
| FailLocked("input not configured before start of stream (QueueInputPacket())"); |
| return false; |
| } |
| |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == stream_queue_.front()->stream_lifetime_ordinal()); |
| stream_ = stream_queue_.front().get(); |
| // Update the stream_lifetime_ordinal_ to the new stream. We need to do |
| // this before we send new output config, since the output config will be |
| // generated using the current stream ordinal. |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal > stream_lifetime_ordinal_); |
| stream_lifetime_ordinal_ = stream_lifetime_ordinal; |
| ZX_DEBUG_ASSERT(stream_->stream_lifetime_ordinal() == stream_lifetime_ordinal_); |
| |
| // The client is not permitted to unilaterally re-configure output while a |
| // stream is active, but the client may still be responding to a previous |
| // server-initiated mid-stream format change. |
| // |
| // ########################################################################### |
| // We don't attempt to optimize every case as much as might be possible here. |
| // The main overall optimization is that it's possible to switch streams |
| // without reallocating buffers. We also need to make sure it's possible to |
| // detect output format at the start of a stream regardless of what happened |
| // before, and possible to perform a mid-stream format change. |
| // ########################################################################### |
| // |
| // Given the above, our *main concern* here is that we get to a state where we |
| // *know* the client isn't trying to re-configure output during format |
| // detection, which at best would be confusing to allow, so we avoid that |
| // possibility here by forcing a client to catch up with the server, if |
| // there's *any possibility* that the client might still be working on |
| // catching up with the server. |
| // |
| // If the client's most recently fully-completed output config is less than |
| // the most recently sent output constraints with action_required true, then |
| // we force an even fresher output constraints here tagged as being relevant |
| // to the current stream, and wait for the client to catch up to that before |
| // continuing. By marking as being for this stream, we ensure that the client |
| // will bother to finish configuring output, which gets us to a state where we |
| // know it's safe to do another mid-stream format change as needed (vs. the |
| // client maybe finishing the old config or maybe not). |
| // |
| // We also force the client to catch up if the core codec previously indicated |
| // that the current config is "meh". This may not be strictly necessary since |
| // the "meh" was with respect to the old stream, but just in case a core codec |
| // cares, we move on from the old config before delivering new stream data. |
| // |
| // Some core codecs may require the output to be configured to _something_ as |
| // they don't support giving us the real output config unless the output is |
| // configured to at least something at first. |
| // |
| // Other core codecs (such as some HW-based codecs) can deal with no output |
| // configured while detecting the output format, but even for those codecs, we |
| // only do this if the above cases don't apply. These codecs have to deal |
| // with an output config that's already set across a stream switch anyway, to |
| // permit buffers to stay configured across a stream switch when possible, so |
| // the cases above potentially setting an output config that's not super |
| // relevant to the new stream doesn't really complicate the core codec since |
| // an old stream's config might not be super relevant to a new stream either. |
| // |
| // Format detection is separate and handled like a mid-stream format change. |
| // This stuff here is just getting output config into a non-changing state |
| // before we start format detection. |
| bool is_new_config_needed; |
| // The statement below could obviously be re-written as a giant boolean |
| // expression, but this way seems easier to comment. |
| if (last_provided_buffer_constraints_version_ordinal_[kOutputPort] < |
| last_required_buffer_constraints_version_ordinal_[kOutputPort]) { |
| // The client _might_ still be trying to catch up, so to disambiguate, |
| // require an even fresher config with respect to this new stream to |
| // unambiguously force the client to catch up to the even newer config. |
| is_new_config_needed = true; |
| } else if (IsCoreCodecRequiringOutputConfigForFormatDetection() && !IsOutputConfiguredLocked()) { |
| // The core codec requires output to be configured before format detection, |
| // so we force the client to provide an output config before format |
| // detection. |
| is_new_config_needed = true; |
| } else if (IsOutputConfiguredLocked() && |
| port_settings_[kOutputPort]->buffer_constraints_version_ordinal() <= |
| core_codec_meh_output_buffer_constraints_version_ordinal_) { |
| // The core codec previously expressed "meh" regarding the current config's |
| // buffer_constraints_version_ordinal, so to avoid mixing that with core |
| // codec stream switch, force the client to configure output buffers before |
| // format detection for the new stream. |
| is_new_config_needed = true; |
| } else { |
| // The core codec is ok to perform format detection in the current state, |
| // and we know that a well-behaved client is not currently trying to change |
| // the output config. |
| is_new_config_needed = false; |
| } |
| |
| if (is_new_config_needed) { |
| StartIgnoringClientOldOutputConfig(lock); |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| // This does count as a mid-stream output config change, even when this is |
| // at the start of a stream - it's still while a stream is active, and still |
| // prevents this stream from outputting any data to the Codec client until |
| // the Codec client re-configures output while this stream is active. |
| GenerateAndSendNewOutputConstraints(lock, true); |
| |
| // Now we can wait for the client to catch up to the current output config |
| // or for the client to tell the server to discard the current stream. |
| while (!IsStoppingLocked() && !stream_->future_discarded() && !IsOutputConfiguredLocked()) { |
| RunAnySysmemCompletionsOrWait(lock); |
| } |
| |
| if (IsStoppingLocked()) { |
| return false; |
| } |
| |
| if (stream_->future_discarded()) { |
| // A discarded stream isn't an error for the CodecImpl instance. |
| return true; |
| } |
| } |
| |
| // Now we have input configured, and output configured if needed by the core |
| // codec, so we can move the core codec to running state. |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecStartStream(); |
| } // ~unlock |
| |
| // Track this so the core codec doesn't have to bother with "ensure" |
| // semantics, just start/stop, where stop isn't called unless the core codec |
| // has a started stream. |
| is_core_codec_stream_started_ = true; |
| |
| return true; |
| } |
| |
| void CodecImpl::EnsureStreamClosed(std::unique_lock<std::mutex>& lock) { |
| VLOGF("EnsureStreamClosed()"); |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| // Stop the core codec, by using this thread to directly drive the core codec |
| // from running to stopped (if not already stopped). We do this first so the |
| // core codec won't try to send us output while we have no stream at the Codec |
| // layer. |
| if (is_core_codec_stream_started_) { |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| VLOGF("CoreCodecStopStream()..."); |
| CoreCodecStopStream(); |
| VLOGF("CoreCodecStopStream() done."); |
| } |
| is_core_codec_stream_started_ = false; |
| } |
| |
| // Now close the old stream at the Codec layer. |
| EnsureCodecStreamClosedLockedInternal(); |
| |
| ZX_DEBUG_ASSERT(!IsStreamActiveLocked()); |
| } |
| |
| // The only valid caller of this is EnsureStreamClosed(). We have this in a |
| // separate method only to make it easier to assert a couple things in the |
| // caller. |
| void CodecImpl::EnsureCodecStreamClosedLockedInternal() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (stream_lifetime_ordinal_ % 2 == 0) { |
| // Already closed. |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_queue_.front()->stream_lifetime_ordinal() == stream_lifetime_ordinal_); |
| stream_ = nullptr; |
| stream_queue_.pop_front(); |
| stream_lifetime_ordinal_++; |
| // Even values mean no current stream. |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal_ % 2 == 0); |
| } |
| |
| bool CodecImpl::RunAnySysmemCompletions(std::unique_lock<std::mutex>& lock) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| // Typically this loop will run once, but on return we want the queue to be |
| // empty even if more showed up while in this method, for condition_variable |
| // signalling reasons. |
| bool any_ran = false; |
| while (!sysmem_completion_queue_.empty()) { |
| // We'll run them all, so extract all the items and run them all. |
| std::queue<fit::closure> local_batch_to_run; |
| local_batch_to_run.swap(sysmem_completion_queue_); |
| // The unlock doesn't cause queue re-ordering, though so far none of these |
| // items care anyway. |
| ScopedUnlock unlock(lock); |
| while (!local_batch_to_run.empty()) { |
| any_ran = true; |
| fit::closure to_run = std::move(local_batch_to_run.front()); |
| local_batch_to_run.pop(); |
| to_run(); |
| } |
| } |
| return any_ran; |
| } |
| |
| void CodecImpl::PostSysmemCompletion(fit::closure to_run) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| sysmem_completion_queue_.emplace(std::move(to_run)); |
| // In case there is no WaitEnsureSysmemReadyOnInput(), we post to |
| // StreamControl to ensure that RunAnySysmemCompletions() runs soon. |
| // Don't let them accumulate though. |
| if (!is_sysmem_runner_pending_) { |
| is_sysmem_runner_pending_ = true; |
| PostToStreamControl([this] { |
| std::unique_lock<std::mutex> lock(lock_); |
| RunAnySysmemCompletions(lock); |
| ZX_DEBUG_ASSERT(sysmem_completion_queue_.empty()); |
| is_sysmem_runner_pending_ = false; |
| }); |
| } |
| } // ~lock |
| |
| // In case to_run needs to get run by a QueueInput...StreamControl() method |
| // via WaitEnsureSysmemReadyOnInput(), we wake the StreamControl thread. We |
| // must do this even if is_sysmem_runner_pending_, in case that runner won't |
| // run for a while due to WaitEnsureSysmemReadyOnInput() blocking |
| // StreamControl. |
| wake_stream_control_condition_.notify_all(); |
| } |
| |
| bool CodecImpl::WaitEnsureSysmemReadyOnInput(std::unique_lock<std::mutex>& lock) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| // Input buffer re-config is not permitted unless there's no current stream. |
| ZX_DEBUG_ASSERT(!IsStreamActiveLocked()); |
| while (!IsInputConfiguredLocked()) { |
| RunAnySysmemCompletionsOrWait(lock); |
| // No need to check for stream switch since it's not permitted for a client |
| // to be sending any message that can cause a new stream until after the |
| // client is done configuring input buffers (enforced elsewhere). |
| if (IsStoppingLocked()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void CodecImpl::RunAnySysmemCompletionsOrWait(std::unique_lock<std::mutex>& lock) { |
| // If any sysmem completions ran, we immediately return, so that conditions |
| // can be checked again in the caller immediately. |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| bool any_completions_ran = RunAnySysmemCompletions(lock); |
| ZX_DEBUG_ASSERT(sysmem_completion_queue_.empty()); |
| if (!any_completions_ran) { |
| // We know sysmem_completion_queue_.empty() and the lock is held just before |
| // this wait(). |
| wake_stream_control_condition_.wait(lock); |
| } |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a message is |
| // received which would be able to start a new stream. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamSeenLocked(uint64_t stream_lifetime_ordinal) { |
| if (future_stream_lifetime_ordinal_ == stream_lifetime_ordinal) { |
| return true; |
| } |
| if (stream_lifetime_ordinal < future_stream_lifetime_ordinal_) { |
| FailLocked("stream_lifetime_ordinal went backward - exiting\n"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal > future_stream_lifetime_ordinal_); |
| if (future_stream_lifetime_ordinal_ % 2 == 1) { |
| if (!EnsureFutureStreamCloseSeenLocked(future_stream_lifetime_ordinal_)) { |
| return false; |
| } |
| } |
| future_stream_lifetime_ordinal_ = stream_lifetime_ordinal; |
| stream_queue_.push_back(std::make_unique<Stream>(stream_lifetime_ordinal)); |
| if (stream_queue_.size() > kMaxInFlightStreams) { |
| FailLocked( |
| "kMaxInFlightStreams reached - clients capable of causing this are " |
| "instead supposed to wait/postpone to prevent this from occurring - " |
| "exiting\n"); |
| return false; |
| } |
| return true; |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a message is |
| // received which would close a stream. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamCloseSeenLocked(uint64_t stream_lifetime_ordinal) { |
| if (future_stream_lifetime_ordinal_ % 2 == 0) { |
| // Already closed. |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_ - 1) { |
| FailLocked( |
| "CloseCurrentStream() seen with stream_lifetime_ordinal != " |
| "most-recent seen stream"); |
| return false; |
| } |
| return true; |
| } |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_) { |
| FailLocked("attempt to close a stream other than the latest seen stream"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == future_stream_lifetime_ordinal_); |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| Stream* closing_stream = stream_queue_.back().get(); |
| ZX_DEBUG_ASSERT(closing_stream->stream_lifetime_ordinal() == stream_lifetime_ordinal); |
| // It is permitted to see a FlushCurrentStream() before a CloseCurrentStream() |
| // and this can make sense if a client just wants to inform the server of all |
| // stream closes, or if the client wants to release_input_buffers or |
| // release_output_buffers after the flush is done. |
| // |
| // If we didn't previously flush, then this close is discarding. |
| if (!closing_stream->future_flush_end_of_stream()) { |
| closing_stream->SetFutureDiscarded(); |
| } |
| future_stream_lifetime_ordinal_++; |
| ZX_DEBUG_ASSERT(future_stream_lifetime_ordinal_ % 2 == 0); |
| return true; |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a flush is |
| // seen. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamFlushSeenLocked(uint64_t stream_lifetime_ordinal) { |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_) { |
| FailLocked("FlushCurrentStream() stream_lifetime_ordinal inconsistent"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| Stream* flushing_stream = stream_queue_.back().get(); |
| // Thanks to the above future_stream_lifetime_ordinal_ check, we know the |
| // future stream is not discarded yet. |
| ZX_DEBUG_ASSERT(!flushing_stream->future_discarded()); |
| if (flushing_stream->future_flush_end_of_stream()) { |
| FailLocked("FlushCurrentStream() used twice on same stream"); |
| return false; |
| } |
| |
| // We don't future-verify that we have a QueueInputEndOfStream(). We'll verify |
| // that later when StreamControl catches up to this stream. |
| |
| // Remember the flush so we later know that a close doesn't imply discard. |
| flushing_stream->SetFutureFlushEndOfStream(); |
| |
| // A FlushEndOfStreamAndCloseStream() is also a close, after the flush. This |
| // keeps future_stream_lifetime_ordinal_ consistent. |
| if (!EnsureFutureStreamCloseSeenLocked(stream_lifetime_ordinal)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // This method is only called when buffer_constraints_action_required will be |
| // true in an OnOutputConstraints() message sent shortly after this method call. |
| // |
| // Even if the client is switching streams rapidly without configuring output, |
| // this method and GenerateAndSendNewOutputConstraints() with |
| // buffer_constraints_action_required true always run in pairs. |
| // |
| // If the client is in the middle of configuring output, we'll start ignoring |
| // the client's messages re. the old buffer_lifetime_ordinal and old |
| // buffer_constraints_version_ordinal until the client catches up to the new |
| // last_required_buffer_constraints_version_ordinal_[kOutputPort]. |
| void CodecImpl::StartIgnoringClientOldOutputConfig(std::unique_lock<std::mutex>& lock) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| |
| // The buffer_lifetime_ordinal_[kOutputPort] can be even on entry due to at |
| // least two cases: 0, and when the client is switching streams repeatedly |
| // without setting a new buffer_lifetime_ordinal_[kOutputPort]. |
| if (buffer_lifetime_ordinal_[kOutputPort] % 2 == 1) { |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] % 2 == 1); |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] == |
| port_settings_[kOutputPort]->buffer_lifetime_ordinal()); |
| buffer_lifetime_ordinal_[kOutputPort]++; |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] % 2 == 0); |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] == |
| port_settings_[kOutputPort]->buffer_lifetime_ordinal() + 1); |
| } |
| |
| // When buffer_constraints_action_required true, we can assert in |
| // GenerateAndSendNewOutputConstraints() that this value is still the |
| // next_output_buffer_constraints_version_ordinal_ in that method. |
| last_required_buffer_constraints_version_ordinal_[kOutputPort] = |
| next_output_buffer_constraints_version_ordinal_; |
| |
| // Now that we've stopped any new calls to CoreCodecRecycleOutputPacket(), |
| // fence through any previously-started call to CoreCodecRecycleOutputPacket() |
| // that maybe have been started previously, before returning from this method. |
| // |
| // We can't just be holding lock_ during the call to |
| // CoreCodecRecycleOutputPacket() because it acquires the video_decoder_lock_ |
| // and in other paths the video_decoder_lock_ is held while acquiring lock_. |
| // |
| // It's ok for the StreamControl domain to wait on the Output domain (but not |
| // the other way around). |
| bool is_output_ordering_domain_done_with_recycle_output_packet = false; |
| std::condition_variable condition_changed; |
| PostToSharedFidl( |
| [this, &is_output_ordering_domain_done_with_recycle_output_packet, &condition_changed] { |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| is_output_ordering_domain_done_with_recycle_output_packet = true; |
| } |
| condition_changed.notify_all(); |
| }); |
| while (!is_output_ordering_domain_done_with_recycle_output_packet) { |
| condition_changed.wait(lock); |
| } |
| ZX_DEBUG_ASSERT(is_output_ordering_domain_done_with_recycle_output_packet); |
| } |
| |
| void CodecImpl::GenerateAndSendNewOutputConstraints(std::unique_lock<std::mutex>& lock, |
| bool buffer_constraints_action_required) { |
| // When client action is required, this can only happen on the StreamControl |
| // ordering domain. When client action is not required, it can happen from |
| // the InputData ordering domain. |
| ZX_DEBUG_ASSERT(buffer_constraints_action_required && thrd_current() == stream_control_thread_ || |
| !buffer_constraints_action_required && IsPotentiallyCoreCodecThread()); |
| |
| uint64_t current_stream_lifetime_ordinal = stream_lifetime_ordinal_; |
| uint64_t new_output_buffer_constraints_version_ordinal = |
| next_output_buffer_constraints_version_ordinal_++; |
| |
| // If buffer_constraints_action_required true, the caller bumped the |
| // last_required_buffer_constraints_version_ordinal_[kOutputPort] before |
| // calling this method (using StartIgnoringClientOldOutputConfig()), to |
| // ensure any output config messages from the client are ignored until the |
| // client catches up to at least |
| // last_required_buffer_constraints_version_ordinal_. |
| ZX_DEBUG_ASSERT(!buffer_constraints_action_required || |
| (last_required_buffer_constraints_version_ordinal_[kOutputPort] == |
| new_output_buffer_constraints_version_ordinal)); |
| |
| std::unique_ptr<const fuchsia::media::StreamOutputConstraints> output_constraints; |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| |
| // Don't call the core codec under the lock_, because we can avoid doing so, |
| // and to allow the core codec to use this thread to call back into |
| // CodecImpl using this stack if needed. So far we don't have any actual |
| // known examples of a core codec using this thread to call back into |
| // CodecImpl using this stack. |
| output_constraints = CoreCodecBuildNewOutputConstraints( |
| current_stream_lifetime_ordinal, new_output_buffer_constraints_version_ordinal, |
| buffer_constraints_action_required); |
| } // ~unlock |
| |
| // We only call GenerateAndSendNewOutputConstraints() from contexts that won't |
| // be changing the stream_lifetime_ordinal_, so the fact that we released the |
| // lock above doesn't mean the stream_lifetime_ordinal_ could have changed, so |
| // we can assert here that it's still the same as above. |
| ZX_DEBUG_ASSERT(current_stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| |
| output_constraints_ = std::move(output_constraints); |
| |
| // Stay under lock after setting output_constraints_, to get proper ordering |
| // of sent messages even if a hostile client deduces the content of this |
| // message before we've sent it and manages to get the server to send another |
| // subsequent OnOutputConstraints(). |
| |
| ZX_DEBUG_ASSERT(sent_buffer_constraints_version_ordinal_[kOutputPort] + 1 == |
| new_output_buffer_constraints_version_ordinal); |
| |
| // Setting this within same lock hold interval as we queue the message to be |
| // sent in order vs. other OnOutputConstraints() messages. This way we can |
| // verify that the client's incoming messages are not trying to configure with |
| // respect to a buffer_constraints_version_ordinal that is newer than we've |
| // actually sent the client. |
| sent_buffer_constraints_version_ordinal_[kOutputPort] = |
| new_output_buffer_constraints_version_ordinal; |
| |
| // Intentional copy of fuchsia::media::OutputConfig output_constraints_ here, |
| // as we want output_constraints_ to remain valid (at least for debugging |
| // reasons for now). |
| PostToSharedFidl([this, output_constraints = fidl::Clone(*output_constraints_)]() mutable { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnOutputConstraints(std::move(output_constraints)); |
| } |
| }); |
| } |
| |
| void CodecImpl::MidStreamOutputConstraintsChange(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| VLOGF("CodecImpl::MidStreamOutputConstraintsChange - stream: %lu", stream_lifetime_ordinal); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| VLOGF("lock aquired 1"); |
| if (stream_lifetime_ordinal < stream_lifetime_ordinal_) { |
| // ignore; The omx_meh_output_buffer_constraints_version_ordinal_ took |
| // care of it. |
| VLOGF("CodecImpl::MidStreamOutputConstraintsChange - stale stream"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| |
| // We can work through the mid-stream output constraints change step by step |
| // using this thread. |
| |
| // This is what starts the interval during which we'll ignore any |
| // in-progress client output config until the client catches up. |
| VLOGF("StartIngoringClientOldOutputConfig()..."); |
| StartIgnoringClientOldOutputConfig(lock); |
| |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| VLOGF("CoreCodecMidStreamOutputBufferReConfigPrepare()..."); |
| CoreCodecMidStreamOutputBufferReConfigPrepare(); |
| } // ~unlock |
| |
| VLOGF("EnsureBuffersNotConfigured()..."); |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| |
| VLOGF("GenerateAndSendNewOutputConstraints()..."); |
| GenerateAndSendNewOutputConstraints(lock, true); |
| |
| // Now we can wait for the client to catch up to the current output config |
| // or for the client to tell the server to discard the current stream. |
| VLOGF("RunAnySysmemCompletionsOrWait()..."); |
| while (!IsStoppingLocked() && !stream_->future_discarded() && !IsOutputConfiguredLocked()) { |
| RunAnySysmemCompletionsOrWait(lock); |
| } |
| |
| if (IsStoppingLocked()) { |
| VLOGF("CodecImpl::MidStreamOutputConstraintsChange IsStoppingLocked()"); |
| return; |
| } |
| |
| if (stream_->future_discarded()) { |
| // We already know how to handle this case, and |
| // core_codec_meh_output_buffer_constraints_version_ordinal_ is still set |
| // such that the client will be forced to re-configure output buffers at |
| // the start of the new stream. |
| VLOGF("CodecImpl::MidStreamOutputConstraintsChange future_discarded()"); |
| return; |
| } |
| |
| // For asserts. |
| VLOGF("ClearMidStreamOutputConstraintsChangeActive()..."); |
| stream_->ClearMidStreamOutputConstraintsChangeActive(); |
| } // ~lock |
| |
| VLOGF("CoreCodecMidStreamOutputBufferReConfigFinish()..."); |
| CoreCodecMidStreamOutputBufferReConfigFinish(); |
| |
| VLOGF("Done with mid-stream format change."); |
| } |
| |
| // TODO(dustingreen): Consider whether we ever intend to plumb anything coming from the core codec |
| // from a different proc. If not (probably this is the case), we can change several of the checks |
| // in here to ZX_DEBUG_ASSERT() instead. |
| bool CodecImpl::FixupBufferCollectionConstraintsLocked( |
| CodecPort port, const fuchsia::media::StreamBufferConstraints& stream_buffer_constraints, |
| const fuchsia::media::StreamBufferPartialSettings& partial_settings, |
| fuchsia::sysmem::BufferCollectionConstraints* buffer_collection_constraints) { |
| fuchsia::sysmem::BufferUsage& usage = buffer_collection_constraints->usage; |
| |
| if (IsCoreCodecMappedBufferUseful(port)) { |
| // Not surprisingly, both decoders and encoders read from input and write to |
| // output. |
| if (port == kInputPort) { |
| if (usage.cpu & ~(fuchsia::sysmem::cpuUsageRead | fuchsia::sysmem::cpuUsageReadOften)) { |
| FailLocked("Core codec set disallowed CPU usage bits (input port)."); |
| return false; |
| } |
| if (!IsPortSecureRequired(kInputPort)) { |
| usage.cpu |= fuchsia::sysmem::cpuUsageRead | fuchsia::sysmem::cpuUsageReadOften; |
| } else { |
| usage.cpu = 0; |
| } |
| } else { |
| if (usage.cpu & ~(fuchsia::sysmem::cpuUsageWrite | fuchsia::sysmem::cpuUsageWriteOften)) { |
| FailLocked("Core codec set disallowed CPU usage bit(s) (output port)."); |
| return false; |
| } |
| if (!IsPortSecureRequired(kOutputPort)) { |
| usage.cpu |= fuchsia::sysmem::cpuUsageWrite | fuchsia::sysmem::cpuUsageWriteOften; |
| } else { |
| usage.cpu = 0; |
| } |
| } |
| } else { |
| if (usage.cpu) { |
| FailLocked("Core codec set usage.cpu despite !IsCoreCodecMappedBufferUseful()"); |
| return false; |
| } |
| // The CPU won't touch the buffers at all. |
| usage.cpu = 0; |
| } |
| if (usage.vulkan) { |
| FailLocked("Core codec set usage.vulkan bits"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(!usage.vulkan); |
| if (usage.display) { |
| FailLocked("Core codec set usage.display bits"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(!usage.display); |
| if (IsDecryptor()) { |
| // DecryptorAdapter should not be setting video usage bits. |
| if (usage.video) { |
| FailLocked("Core codec set disallowed video usage bits for decryptor"); |
| return false; |
| } |
| if (port == kOutputPort) { |
| usage.video |= fuchsia::sysmem::videoUsageDecryptorOutput; |
| } |
| } else if (IsCoreCodecHwBased(port)) { |
| // Let's see if we can deprecate videoUsageHwProtected, since it's redundant |
| // with secure_required. |
| if (usage.video & fuchsia::sysmem::videoUsageHwProtected) { |
| FailLocked("Core codec set deprecated videoUsageHwProtected - disallow"); |
| return false; |
| } |
| uint32_t allowed_video_usage_bits = |
| IsDecoder() ? fuchsia::sysmem::videoUsageHwDecoder : fuchsia::sysmem::videoUsageHwEncoder; |
| if (usage.video & ~allowed_video_usage_bits) { |
| FailLocked( |
| "Core codec set disallowed video usage bit(s) - port: %d, usage: " |
| "0x%08x, allowed: 0x%08x", |
| port, usage.video, allowed_video_usage_bits); |
| return false; |
| } |
| if (IsDecoder()) { |
| usage.video |= fuchsia::sysmem::videoUsageHwDecoder; |
| } else if (IsEncoder()) { |
| usage.video |= fuchsia::sysmem::videoUsageHwEncoder; |
| } |
| } else { |
| // Despite being a video decoder or encoder, a SW decoder or encoder doesn't |
| // count as videoUsageHwDecoder or videoUsageHwEncoder. And definitely not |
| // videoUsageHwProtected. |
| usage.video = 0; |
| } |
| |
| bool is_single_buffer_mode = |
| partial_settings.has_single_buffer_mode() && partial_settings.single_buffer_mode(); |
| |
| uint32_t required_min_buffer_count_for_camping; |
| if (is_single_buffer_mode) { |
| // This isn't especially meaningful for is_single_buffer_mode. |
| required_min_buffer_count_for_camping = |
| static_cast<uint32_t>(stream_buffer_constraints.packet_count_for_server_min() != 0); |
| } else { |
| required_min_buffer_count_for_camping = stream_buffer_constraints.packet_count_for_server_min(); |
| } |
| if (buffer_collection_constraints->min_buffer_count_for_camping < |
| required_min_buffer_count_for_camping) { |
| FailLocked( |
| "Core codec set min_buffer_count_for_camping too low - " |
| "min_buffer_count_for_camping: %lu " |
| "required_min_buffer_count_for_camping: %lu", |
| buffer_collection_constraints->min_buffer_count_for_camping, |
| required_min_buffer_count_for_camping); |
| return false; |
| } |
| |
| if (is_single_buffer_mode) { |
| if (buffer_collection_constraints->min_buffer_count_for_camping > 1) { |
| FailLocked( |
| "Core codec set min_buffer_count_for_camping too high for " |
| "single_buffer_mode - min_buffer_count_for_camping: %lu", |
| buffer_collection_constraints->min_buffer_count_for_camping); |
| return false; |
| } |
| if (buffer_collection_constraints->min_buffer_count_for_dedicated_slack != 0 || |
| buffer_collection_constraints->min_buffer_count_for_shared_slack != 0) { |
| FailLocked( |
| "Core codec set slack with single_buffer_mode - " |
| "min_buffer_count_for_dedicated_slack: %lu " |
| "min_buffer_count_for_shared_slack: %lu", |
| buffer_collection_constraints->min_buffer_count_for_dedicated_slack, |
| buffer_collection_constraints->min_buffer_count_for_shared_slack); |
| return false; |
| } |
| if (buffer_collection_constraints->max_buffer_count != 1) { |
| FailLocked("Core codec must specify max_buffer_count 1 when single_buffer_mode"); |
| return false; |
| } |
| } |
| |
| if (!buffer_collection_constraints->has_buffer_memory_constraints) { |
| // Leaving all fields set to their defaults is fine if that's really true, but this encourages |
| // CodecAdapter implementations to set fields in here. |
| FailLocked("Core codec must set has_buffer_memory_constraints"); |
| return false; |
| } |
| fuchsia::sysmem::BufferMemoryConstraints& buffer_memory_constraints = |
| buffer_collection_constraints->buffer_memory_constraints; |
| |
| // Sysmem will fail the BufferCollection if the core codec provides constraints that are |
| // inconsistent, but we need to check here that the core codec is being consistent with |
| // SecureMemoryMode, since sysmem doesn't know about SecureMemoryMode. Essentially |
| // SecureMemoryMode translates into secure_required and secure_permitted in sysmem. The former |
| // is just a bool. The latter is indicated by listing at least one secure heap. |
| |
| // secure_required consistency check |
| // |
| // CoreCodecSetSecureMemoryMode() informed the core codec of the mode previously. |
| if (!!IsPortSecureRequired(port) != !!buffer_memory_constraints.secure_required) { |
| FailLocked("Core codec secure_required inconsistent with SecureMemoryMode"); |
| return false; |
| } |
| |
| // secure_permitted consistency check |
| // |
| // If secure is permitted, then the core codec must support at least one non-SYSTEM_RAM heap, as |
| // specifying support for a secure heap is how sysmem knows secure_permitted. We can't directly |
| // tell that the non-RAM heap is secure, so this is an approximate check. In any case |
| // secure_required by any sysmem participant will be enforced by sysmem with respect to specific |
| // heaps and whether they're secure. The approximate-ness is ok since this only comes from |
| // in-proc, so the check is just for trying to notice if the core codec is filling out |
| // inconsistent constraints in a way that sysmem wouldn't otherwise notice. |
| bool is_non_ram_heap_found = false; |
| for (uint32_t iter = 0; iter < buffer_memory_constraints.heap_permitted_count; ++iter) { |
| if (buffer_memory_constraints.heap_permitted[iter] != fuchsia::sysmem::HeapType::SYSTEM_RAM) { |
| is_non_ram_heap_found = true; |
| break; |
| } |
| } |
| if (IsPortSecurePermitted(port) && !is_non_ram_heap_found) { |
| FailLocked("Core codec must specify at least one non-RAM heap when secure_required"); |
| return false; |
|