| // 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 "amlogic-video.h" |
| |
| #include <lib/trace/event.h> |
| #include <lib/zx/channel.h> |
| #include <memory.h> |
| #include <stdint.h> |
| #include <zircon/errors.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/smc.h> |
| |
| #include <chrono> |
| #include <memory> |
| #include <optional> |
| #include <thread> |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <ddk/platform-defs.h> |
| #include <ddk/protocol/platform/device.h> |
| #include <ddktl/protocol/composite.h> |
| #include <hw/reg.h> |
| #include <hwreg/bitfields.h> |
| #include <hwreg/mmio.h> |
| |
| #include "device_ctx.h" |
| #include "device_fidl.h" |
| #include "hevcdec.h" |
| #include "local_codec_factory.h" |
| #include "macros.h" |
| #include "mpeg12_decoder.h" |
| #include "pts_manager.h" |
| #include "registers.h" |
| #include "src/media/lib/memory_barriers/memory_barriers.h" |
| #include "util.h" |
| #include "vdec1.h" |
| #include "video_firmware_session.h" |
| |
| // TODO(fxbug.dev/35200): |
| // |
| // AllocateIoBuffer() - only used by VP9 - switch to InternalBuffer when we do zero copy on input |
| // for VP9. |
| // |
| // (AllocateStreamBuffer() has been moved to InternalBuffer.) |
| // (VideoDecoder::Owner::ProtectableHardwareUnit::kParser pays attention to is_secure.) |
| // |
| // (Fine as io_buffer_t, at least for now (for both h264 and VP9): |
| // search_pattern_ - HW only reads this |
| // parser_input_ - not used when secure) |
| |
| // TODO(fxbug.dev/41972): bti::release_quarantine() or zx_bti_release_quarantine() somewhere during |
| // startup, after HW is known idle, before we allocate anything from sysmem. |
| |
| namespace { |
| |
| // These match the regions exported when the bus device was added. |
| enum MmioRegion { |
| kCbus, |
| kDosbus, |
| kHiubus, |
| kAobus, |
| kDmc, |
| }; |
| |
| enum Interrupt { |
| kDemuxIrq, |
| kParserIrq, |
| kDosMbox0Irq, |
| kDosMbox1Irq, |
| }; |
| |
| } // namespace |
| |
| AmlogicVideo::AmlogicVideo() { |
| ZX_DEBUG_ASSERT(metrics_ == &default_nop_metrics_); |
| vdec1_core_ = std::make_unique<Vdec1>(this); |
| hevc_core_ = std::make_unique<HevcDec>(this); |
| } |
| |
| AmlogicVideo::~AmlogicVideo() { |
| LOG(INFO, "Tearing down AmlogicVideo"); |
| if (vdec0_interrupt_handle_) { |
| zx_interrupt_destroy(vdec0_interrupt_handle_.get()); |
| if (vdec0_interrupt_thread_.joinable()) |
| vdec0_interrupt_thread_.join(); |
| } |
| if (vdec1_interrupt_handle_) { |
| zx_interrupt_destroy(vdec1_interrupt_handle_.get()); |
| if (vdec1_interrupt_thread_.joinable()) |
| vdec1_interrupt_thread_.join(); |
| } |
| swapped_out_instances_.clear(); |
| current_instance_ = nullptr; |
| core_ = nullptr; |
| hevc_core_ = nullptr; |
| vdec1_core_ = nullptr; |
| } |
| |
| // TODO: Remove once we can add single-instance decoders through |
| // AddNewDecoderInstance. |
| void AmlogicVideo::SetDefaultInstance(std::unique_ptr<VideoDecoder> decoder, bool hevc) { |
| DecoderCore* core = hevc ? hevc_core_.get() : vdec1_core_.get(); |
| assert(!stream_buffer_); |
| assert(!current_instance_); |
| current_instance_ = std::make_unique<DecoderInstance>(std::move(decoder), core); |
| video_decoder_ = current_instance_->decoder(); |
| stream_buffer_ = current_instance_->stream_buffer(); |
| core_ = core; |
| } |
| |
| void AmlogicVideo::AddNewDecoderInstance(std::unique_ptr<DecoderInstance> instance) { |
| swapped_out_instances_.push_back(std::move(instance)); |
| } |
| |
| void AmlogicVideo::UngateClocks() { |
| ToggleClock(ClockType::kClkDos, true); |
| HhiGclkMpeg1::Get().ReadFrom(&*hiubus_).set_aiu(0xff).set_demux(true).set_audio_in(true).WriteTo( |
| &*hiubus_); |
| HhiGclkMpeg2::Get().ReadFrom(&*hiubus_).set_vpu_interrupt(true).WriteTo(&*hiubus_); |
| UngateParserClock(); |
| } |
| |
| void AmlogicVideo::UngateParserClock() { |
| is_parser_gated_ = false; |
| HhiGclkMpeg1::Get().ReadFrom(&*hiubus_).set_u_parser_top(true).WriteTo(&*hiubus_); |
| } |
| |
| void AmlogicVideo::GateClocks() { |
| // Keep VPU interrupt enabled, as it's used for vsync by the display. |
| HhiGclkMpeg1::Get() |
| .ReadFrom(&*hiubus_) |
| .set_u_parser_top(false) |
| .set_aiu(0) |
| .set_demux(false) |
| .set_audio_in(false) |
| .WriteTo(&*hiubus_); |
| ToggleClock(ClockType::kClkDos, false); |
| GateParserClock(); |
| } |
| |
| void AmlogicVideo::GateParserClock() { |
| is_parser_gated_ = true; |
| HhiGclkMpeg1::Get().ReadFrom(&*hiubus_).set_u_parser_top(false).WriteTo(&*hiubus_); |
| } |
| |
| void AmlogicVideo::ClearDecoderInstance() { |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| assert(current_instance_); |
| assert(swapped_out_instances_.size() == 0); |
| LOG(DEBUG, "current_instance_.reset()..."); |
| current_instance_ = nullptr; |
| core_ = nullptr; |
| video_decoder_ = nullptr; |
| stream_buffer_ = nullptr; |
| } |
| |
| void AmlogicVideo::RemoveDecoder(VideoDecoder* decoder) { |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| RemoveDecoderLocked(decoder); |
| } |
| |
| void AmlogicVideo::RemoveDecoderLocked(VideoDecoder* decoder) { |
| DLOG("Removing decoder: %p", decoder); |
| ZX_DEBUG_ASSERT(decoder); |
| if (current_instance_ && current_instance_->decoder() == decoder) { |
| current_instance_ = nullptr; |
| video_decoder_ = nullptr; |
| stream_buffer_ = nullptr; |
| core_ = nullptr; |
| TryToReschedule(); |
| return; |
| } |
| for (auto it = swapped_out_instances_.begin(); it != swapped_out_instances_.end(); ++it) { |
| if ((*it)->decoder() != decoder) |
| continue; |
| swapped_out_instances_.erase(it); |
| return; |
| } |
| } |
| |
| zx_status_t AmlogicVideo::AllocateStreamBuffer(StreamBuffer* buffer, uint32_t size, bool use_parser, |
| bool is_secure) { |
| // So far, is_secure can only be true if use_parser is also true. |
| ZX_DEBUG_ASSERT(!is_secure || use_parser); |
| // is_writable is always true because we either need to write into this buffer using the CPU, or |
| // using the parser - either way we'll be writing. |
| auto create_result = |
| InternalBuffer::Create("AMLStreamBuffer", &sysmem_sync_ptr_, zx::unowned_bti(bti_), size, |
| is_secure, /*is_writable=*/true, /*is_mapping_needed=*/!use_parser); |
| if (!create_result.is_ok()) { |
| DECODE_ERROR("Failed to make video fifo: %d", create_result.error()); |
| return create_result.error(); |
| } |
| buffer->optional_buffer().emplace(create_result.take_value()); |
| |
| // Sysmem guarantees that the newly-allocated buffer starts out zeroed and cache clean, to the |
| // extent possible based on is_secure. |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlogicVideo::ConnectToTee(fuchsia::tee::DeviceSyncPtr* tee) { |
| ZX_DEBUG_ASSERT(tee); |
| |
| zx::channel tee_client; |
| zx::channel tee_server; |
| zx_status_t status = zx::channel::create(/*flags=*/0, &tee_client, &tee_server); |
| if (status != ZX_OK) { |
| LOG(ERROR, "zx::channel::create() failed - status: %d", status); |
| return status; |
| } |
| |
| status = tee_.Connect(std::move(tee_server), /*service_provider=*/zx::channel()); |
| if (status != ZX_OK) { |
| LOG(ERROR, "tee_connect() failed - status: %d", status); |
| return status; |
| } |
| |
| tee->Bind(std::move(tee_client)); |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlogicVideo::EnsureSecmemSessionIsConnected() { |
| if (secmem_session_.has_value()) { |
| return ZX_OK; |
| } |
| |
| fuchsia::tee::DeviceSyncPtr tee_connection; |
| zx_status_t status = ConnectToTee(&tee_connection); |
| if (status != ZX_OK) { |
| LOG(ERROR, "ConnectToTee() failed - status: %d", status); |
| return status; |
| } |
| |
| auto secmem_session_result = SecmemSession::TryOpen(std::move(tee_connection)); |
| if (!secmem_session_result.is_ok()) { |
| // Logging handled in `SecmemSession::TryOpen` |
| return ZX_ERR_INTERNAL; |
| } |
| |
| secmem_session_.emplace(secmem_session_result.take_value()); |
| return ZX_OK; |
| } |
| |
| void AmlogicVideo::InitializeStreamInput(bool use_parser) { |
| uint32_t buffer_address = truncate_to_32(stream_buffer_->buffer().phys_base()); |
| auto buffer_size = stream_buffer_->buffer().size(); |
| core_->InitializeStreamInput(use_parser, buffer_address, buffer_size); |
| } |
| |
| zx_status_t AmlogicVideo::InitializeStreamBuffer(bool use_parser, uint32_t size, bool is_secure) { |
| zx_status_t status = AllocateStreamBuffer(stream_buffer_, size, use_parser, is_secure); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = SetProtected(VideoDecoder::Owner::ProtectableHardwareUnit::kParser, is_secure); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| InitializeStreamInput(use_parser); |
| return ZX_OK; |
| } |
| |
| std::unique_ptr<CanvasEntry> AmlogicVideo::ConfigureCanvas(io_buffer_t* io_buffer, uint32_t offset, |
| uint32_t width, uint32_t height, |
| uint32_t wrap, uint32_t blockmode) { |
| assert(width % 8 == 0); |
| assert(offset % 8 == 0); |
| canvas_info_t info; |
| info.height = height; |
| info.stride_bytes = width; |
| info.wrap = wrap; |
| info.blkmode = blockmode; |
| enum { |
| kSwapBytes = 1, |
| kSwapWords = 2, |
| kSwapDoublewords = 4, |
| kSwapQuadwords = 8, |
| }; |
| info.endianness = |
| kSwapBytes | kSwapWords | kSwapDoublewords; // 64-bit big-endian to little-endian conversion. |
| info.flags = CANVAS_FLAGS_READ | CANVAS_FLAGS_WRITE; |
| |
| zx::unowned_vmo vmo(io_buffer->vmo_handle); |
| zx::vmo dup_vmo; |
| zx_status_t status = vmo->duplicate(ZX_RIGHT_SAME_RIGHTS, &dup_vmo); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed to duplicate handle, status: %d", status); |
| return nullptr; |
| } |
| uint8_t idx; |
| status = canvas_.Config(std::move(dup_vmo), offset, &info, &idx); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed to configure canvas, status: %d", status); |
| return nullptr; |
| } |
| |
| return std::make_unique<CanvasEntry>(this, idx); |
| } |
| |
| void AmlogicVideo::FreeCanvas(CanvasEntry* canvas) { canvas_.Free(canvas->index()); } |
| |
| void AmlogicVideo::OnSignaledWatchdog() { |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| // Check after taking lock to ensure a cancel didn't just happen. |
| if (!watchdog_.CheckAndResetTimeout()) |
| return; |
| // The watchdog should never be valid if the decoder was disconnected. |
| ZX_ASSERT(video_decoder_); |
| video_decoder_->OnSignaledWatchdog(); |
| } |
| |
| zx_status_t AmlogicVideo::AllocateIoBuffer(io_buffer_t* buffer, size_t size, |
| uint32_t alignment_log2, uint32_t flags, |
| const char* name) { |
| zx_status_t status = io_buffer_init_aligned(buffer, bti_.get(), size, alignment_log2, flags); |
| if (status != ZX_OK) |
| return status; |
| |
| SetIoBufferName(buffer, name); |
| |
| return ZX_OK; |
| } |
| |
| fuchsia::sysmem::AllocatorSyncPtr& AmlogicVideo::SysmemAllocatorSyncPtr() { |
| return sysmem_sync_ptr_; |
| } |
| |
| // This parser handles MPEG elementary streams. |
| zx_status_t AmlogicVideo::InitializeEsParser() { |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| return parser_->InitializeEsParser(current_instance_.get()); |
| } |
| |
| uint32_t AmlogicVideo::GetStreamBufferEmptySpaceAfterWriteOffsetBeforeReadOffset( |
| uint32_t write_offset, uint32_t read_offset) { |
| uint32_t available_space; |
| if (read_offset > write_offset) { |
| available_space = read_offset - write_offset; |
| } else { |
| available_space = stream_buffer_->buffer().size() - write_offset + read_offset; |
| } |
| // Subtract 8 to ensure the read pointer doesn't become equal to the write |
| // pointer, as that means the buffer is empty. |
| available_space = available_space > 8 ? available_space - 8 : 0; |
| return available_space; |
| } |
| |
| uint32_t AmlogicVideo::GetStreamBufferEmptySpaceAfterOffset(uint32_t write_offset) { |
| uint32_t read_offset = core_->GetReadOffset(); |
| return GetStreamBufferEmptySpaceAfterWriteOffsetBeforeReadOffset(write_offset, read_offset); |
| } |
| |
| uint32_t AmlogicVideo::GetStreamBufferEmptySpace() { |
| return GetStreamBufferEmptySpaceAfterOffset(core_->GetStreamInputOffset()); |
| } |
| |
| zx_status_t AmlogicVideo::ProcessVideoNoParser(const void* data, uint32_t len, |
| uint32_t* written_out) { |
| return ProcessVideoNoParserAtOffset(data, len, core_->GetStreamInputOffset(), written_out); |
| } |
| |
| zx_status_t AmlogicVideo::ProcessVideoNoParserAtOffset(const void* data, uint32_t len, |
| uint32_t write_offset, |
| uint32_t* written_out) { |
| TRACE_DURATION("media", "AmlogicVideo::ProcessVideoNoParserAtOffset"); |
| uint32_t available_space = GetStreamBufferEmptySpaceAfterOffset(write_offset); |
| if (!written_out) { |
| if (len > available_space) { |
| DECODE_ERROR("Video too large"); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| } else { |
| len = std::min(len, available_space); |
| *written_out = len; |
| } |
| |
| stream_buffer_->set_data_size(stream_buffer_->data_size() + len); |
| uint32_t input_offset = 0; |
| while (len > 0) { |
| uint32_t write_length = len; |
| if (write_offset + len > stream_buffer_->buffer().size()) |
| write_length = stream_buffer_->buffer().size() - write_offset; |
| memcpy(stream_buffer_->buffer().virt_base() + write_offset, |
| static_cast<const uint8_t*>(data) + input_offset, write_length); |
| stream_buffer_->buffer().CacheFlush(write_offset, write_length); |
| write_offset += write_length; |
| if (write_offset == stream_buffer_->buffer().size()) |
| write_offset = 0; |
| len -= write_length; |
| input_offset += write_length; |
| } |
| BarrierAfterFlush(); |
| core_->UpdateWritePointer(stream_buffer_->buffer().phys_base() + write_offset); |
| return ZX_OK; |
| } |
| |
| void AmlogicVideo::SwapOutCurrentInstance() { |
| TRACE_DURATION("media", "AmlogicVideo::SwapOutCurrentInstance", "current_instance_", |
| current_instance_.get()); |
| ZX_DEBUG_ASSERT(!!current_instance_); |
| |
| // VP9: |
| // |
| // FrameWasOutput() is called during handling of kVp9CommandNalDecodeDone on the interrupt thread, |
| // which means the decoder HW is currently paused, which means it's ok to save the state before |
| // the stop+wait (without any explicit pause before the save here). The decoder HW remains paused |
| // after the save, and makes no further progress until later after the restore. |
| // |
| // h264_multi_decoder: |
| // |
| // ShouldSaveInputContext() is true if the h264_multi_decoder made useful progress (decoded a |
| // picture). If no useful progress was made, the lack of save here allows the state restore later |
| // to effectively back up and try decoding from the same location again, with more data present. |
| // This backing up to the previous saved state is the main way that separate SPS PPS and pictures |
| // split across packets are handled. In other words, it's how the h264_multi_decoder handles |
| // stream-based input. |
| bool should_save = current_instance_->decoder()->ShouldSaveInputContext(); |
| DLOG("should_save: %d", should_save); |
| if (should_save) { |
| if (!current_instance_->input_context()) { |
| current_instance_->InitializeInputContext(); |
| if (core_->InitializeInputContext(current_instance_->input_context(), |
| current_instance_->decoder()->is_secure()) != ZX_OK) { |
| video_decoder_->CallErrorHandler(); |
| // Continue trying to swap out. |
| } |
| } |
| } |
| video_decoder_->SetSwappedOut(); |
| if (should_save) { |
| if (current_instance_->input_context()) { |
| if (core_->SaveInputContext(current_instance_->input_context()) != ZX_OK) { |
| video_decoder_->CallErrorHandler(); |
| // Continue trying to swap out. |
| } |
| } |
| } |
| video_decoder_ = nullptr; |
| stream_buffer_ = nullptr; |
| core_->StopDecoding(); |
| core_->WaitForIdle(); |
| core_ = nullptr; |
| // Round-robin; place at the back of the line. |
| swapped_out_instances_.push_back(std::move(current_instance_)); |
| } |
| |
| void AmlogicVideo::TryToReschedule() { |
| TRACE_DURATION("media", "AmlogicVideo::TryToReschedule"); |
| DLOG("AmlogicVideo::TryToReschedule"); |
| |
| if (current_instance_ && !current_instance_->decoder()->CanBeSwappedOut()) { |
| DLOG("Current instance can't be swapped out"); |
| return; |
| } |
| |
| // This is used by h264_multi_decoder to swap out without saving, so that the next swap in will |
| // restore a previously-saved state again to re-attempt decode from that saved state's logical |
| // read start position. Unlike the read position which backs up for re-decode, the write position |
| // is adjusted after restore to avoid overwriting data written since that save state was |
| // originally created. |
| if (current_instance_ && current_instance_->decoder()->MustBeSwappedOut()) { |
| DLOG("MustBeSwappedOut() is true"); |
| SwapOutCurrentInstance(); |
| } |
| |
| if (current_instance_ && current_instance_->decoder()->test_hooks().force_context_save_restore) { |
| SwapOutCurrentInstance(); |
| } |
| |
| if (swapped_out_instances_.size() == 0) { |
| DLOG("Nothing swapped out; returning"); |
| return; |
| } |
| |
| // Round-robin; first in line that can be swapped in goes first. |
| // TODO: Use some priority mechanism to determine which to swap in. |
| auto other_instance = swapped_out_instances_.begin(); |
| for (; other_instance != swapped_out_instances_.end(); ++other_instance) { |
| if ((*other_instance)->decoder()->CanBeSwappedIn()) { |
| break; |
| } |
| } |
| if (other_instance == swapped_out_instances_.end()) { |
| DLOG("nothing to swap to"); |
| return; |
| } |
| |
| ZX_ASSERT(!watchdog_.is_running()); |
| if (current_instance_) { |
| SwapOutCurrentInstance(); |
| } |
| current_instance_ = std::move(*other_instance); |
| swapped_out_instances_.erase(other_instance); |
| |
| SwapInCurrentInstance(); |
| } |
| |
| void AmlogicVideo::PowerOffForError() { |
| ZX_DEBUG_ASSERT(core_); |
| core_ = nullptr; |
| swapped_out_instances_.push_back(std::move(current_instance_)); |
| VideoDecoder* video_decoder = video_decoder_; |
| video_decoder_ = nullptr; |
| stream_buffer_ = nullptr; |
| video_decoder->CallErrorHandler(); |
| // CallErrorHandler should have marked the decoder as having a fatal error |
| // so it will never be rescheduled. |
| TryToReschedule(); |
| } |
| |
| void AmlogicVideo::SwapInCurrentInstance() { |
| TRACE_DURATION("media", "AmlogicVideo::SwapInCurrentInstance", "current_instance_", |
| current_instance_.get()); |
| ZX_DEBUG_ASSERT(current_instance_); |
| |
| core_ = current_instance_->core(); |
| video_decoder_ = current_instance_->decoder(); |
| DLOG("Swapping in %p", video_decoder_); |
| stream_buffer_ = current_instance_->stream_buffer(); |
| { |
| zx_status_t status = video_decoder_->SetupProtection(); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed to setup protection: %d", status); |
| PowerOffForError(); |
| return; |
| } |
| } |
| if (!current_instance_->input_context()) { |
| InitializeStreamInput(false); |
| core_->InitializeDirectInput(); |
| // If data has added to the stream buffer before the first swap in(only |
| // relevant in tests right now) then ensure the write pointer's updated to |
| // that spot. |
| // Generally data will only be added after this decoder is swapped in, so |
| // RestoreInputContext will handle that state. |
| if (stream_buffer_->data_size() + stream_buffer_->padding_size() > 0) { |
| core_->UpdateWritePointer(stream_buffer_->buffer().phys_base() + stream_buffer_->data_size() + |
| stream_buffer_->padding_size()); |
| } |
| } else { |
| if (core_->RestoreInputContext(current_instance_->input_context()) != ZX_OK) { |
| PowerOffForError(); |
| return; |
| } |
| } |
| |
| // Do InitializeHardware after setting up the input context, since for H264Multi the vififo can |
| // start reading as soon as PowerCtlVld is set up (inside InitializeHardware), and we don't want |
| // it to read incorrect data as we gradually set it up later. |
| zx_status_t status = video_decoder_->InitializeHardware(); |
| if (status != ZX_OK) { |
| // Probably failed to load the right firmware. |
| DECODE_ERROR("Failed to initialize hardware: %d", status); |
| PowerOffForError(); |
| return; |
| } |
| video_decoder_->SwappedIn(); |
| } |
| |
| fidl::InterfaceHandle<fuchsia::sysmem::Allocator> AmlogicVideo::ConnectToSysmem() { |
| fidl::InterfaceHandle<fuchsia::sysmem::Allocator> client_end; |
| fidl::InterfaceRequest<fuchsia::sysmem::Allocator> server_end = client_end.NewRequest(); |
| zx_status_t connect_status = sysmem_.Connect(server_end.TakeChannel()); |
| if (connect_status != ZX_OK) { |
| // failure |
| return fidl::InterfaceHandle<fuchsia::sysmem::Allocator>(); |
| } |
| return client_end; |
| } |
| |
| namespace tee_smc { |
| |
| enum CallType : uint8_t { |
| kYieldingCall = 0, |
| kFastCall = 1, |
| }; |
| |
| enum CallConvention : uint8_t { |
| kSmc32CallConv = 0, |
| kSmc64CallConv = 1, |
| }; |
| |
| enum Service : uint8_t { |
| kArchService = 0x00, |
| kCpuService = 0x01, |
| kSipService = 0x02, |
| kOemService = 0x03, |
| kStandardService = 0x04, |
| kTrustedOsService = 0x32, |
| kTrustedOsServiceEnd = 0x3F, |
| }; |
| |
| constexpr uint8_t kCallTypeMask = 0x01; |
| constexpr uint8_t kCallTypeShift = 31; |
| constexpr uint8_t kCallConvMask = 0x01; |
| constexpr uint8_t kCallConvShift = 30; |
| constexpr uint8_t kServiceMask = ARM_SMC_SERVICE_CALL_NUM_MASK; |
| constexpr uint8_t kServiceShift = ARM_SMC_SERVICE_CALL_NUM_SHIFT; |
| |
| static constexpr uint32_t CreateFunctionId(CallType call_type, CallConvention call_conv, |
| Service service, uint16_t function_num) { |
| return (((call_type & kCallTypeMask) << kCallTypeShift) | |
| ((call_conv & kCallConvMask) << kCallConvShift) | |
| ((service & kServiceMask) << kServiceShift) | function_num); |
| } |
| } // namespace tee_smc |
| |
| zx_status_t AmlogicVideo::SetProtected(ProtectableHardwareUnit unit, bool protect) { |
| TRACE_DURATION("media", "AmlogicVideo::SetProtected", "unit", static_cast<uint32_t>(unit), |
| "protect", protect); |
| if (!secure_monitor_) |
| return protect ? ZX_ERR_INVALID_ARGS : ZX_OK; |
| |
| // Call into the TEE to mark a particular hardware unit as able to access |
| // protected memory or not. |
| zx_smc_parameters_t params = {}; |
| zx_smc_result_t result = {}; |
| constexpr uint32_t kFuncIdConfigDeviceSecure = 14; |
| params.func_id = tee_smc::CreateFunctionId(tee_smc::kFastCall, tee_smc::kSmc32CallConv, |
| tee_smc::kTrustedOsService, kFuncIdConfigDeviceSecure); |
| params.arg1 = static_cast<uint32_t>(unit); |
| params.arg2 = static_cast<uint32_t>(protect); |
| zx_status_t status = zx_smc_call(secure_monitor_.get(), ¶ms, &result); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed to set unit %ld protected status %ld code: %d", params.arg1, params.arg2, |
| status); |
| return status; |
| } |
| if (result.arg0 != 0) { |
| DECODE_ERROR("Failed to set unit %ld protected status %ld: %lx", params.arg1, params.arg2, |
| result.arg0); |
| return ZX_ERR_INTERNAL; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlogicVideo::TeeSmcLoadVideoFirmware(FirmwareBlob::FirmwareType index, |
| FirmwareBlob::FirmwareVdecLoadMode vdec) { |
| ZX_DEBUG_ASSERT(is_tee_available()); |
| ZX_DEBUG_ASSERT(secure_monitor_); |
| |
| // Call into the TEE to tell the HW to use a particular piece of the previously pre-loaded overall |
| // firmware blob. |
| zx_smc_parameters_t params = {}; |
| zx_smc_result_t result = {}; |
| constexpr uint32_t kFuncIdLoadVideoFirmware = 15; |
| params.func_id = tee_smc::CreateFunctionId(tee_smc::kFastCall, tee_smc::kSmc32CallConv, |
| tee_smc::kTrustedOsService, kFuncIdLoadVideoFirmware); |
| params.arg1 = static_cast<uint32_t>(index); |
| params.arg2 = static_cast<uint32_t>(vdec); |
| zx_status_t status = zx_smc_call(secure_monitor_.get(), ¶ms, &result); |
| if (status != ZX_OK) { |
| LOG(ERROR, "Failed to kFuncIdLoadVideoFirmware - index: %u vdec: %u status: %d", index, vdec, |
| status); |
| return status; |
| } |
| if (result.arg0 != 0) { |
| LOG(ERROR, "kFuncIdLoadVideoFirmware result.arg0 != 0 - value: %lu", result.arg0); |
| return ZX_ERR_INTERNAL; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlogicVideo::TeeVp9AddHeaders(zx_paddr_t page_phys_base, uint32_t before_size, |
| uint32_t max_after_size, uint32_t* after_size) { |
| ZX_DEBUG_ASSERT(after_size); |
| ZX_DEBUG_ASSERT(is_tee_available()); |
| |
| // TODO(fxbug.dev/44674): Remove this retry loop once this issue is resolved. |
| constexpr uint32_t kRetryCount = 20; |
| zx_status_t status = ZX_OK; |
| for (uint32_t i = 0; i < kRetryCount; ++i) { |
| status = EnsureSecmemSessionIsConnected(); |
| if (status != ZX_OK) { |
| continue; |
| } |
| |
| status = |
| secmem_session_->GetVp9HeaderSize(page_phys_base, before_size, max_after_size, after_size); |
| if (status != ZX_OK) { |
| LOG(ERROR, "secmem_session_->GetVp9HeaderSize() failed - status: %d", status); |
| |
| // Explicitly disconnect and clean up `secmem_session_`. |
| secmem_session_ = std::nullopt; |
| continue; |
| } |
| |
| ZX_DEBUG_ASSERT(*after_size <= max_after_size); |
| return ZX_OK; |
| } |
| |
| return status; |
| } |
| |
| void AmlogicVideo::ToggleClock(ClockType type, bool enable) { |
| if (enable) { |
| clocks_[static_cast<int>(type)].Enable(); |
| } else { |
| clocks_[static_cast<int>(type)].Disable(); |
| } |
| } |
| |
| void AmlogicVideo::SetMetrics(CodecMetrics* metrics) { |
| ZX_DEBUG_ASSERT(metrics); |
| ZX_DEBUG_ASSERT(metrics != &default_nop_metrics_); |
| ZX_DEBUG_ASSERT(metrics_ == &default_nop_metrics_); |
| metrics_ = metrics; |
| } |
| |
| zx_status_t AmlogicVideo::InitRegisters(zx_device_t* parent) { |
| parent_ = parent; |
| |
| ddk::CompositeProtocolClient composite(parent); |
| if (!composite.is_valid()) { |
| DECODE_ERROR("Could not get composite protocol"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| pdev_ = ddk::PDev(composite); |
| if (!pdev_.is_valid()) { |
| DECODE_ERROR("Failed to get pdev protocol"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| sysmem_ = ddk::SysmemProtocolClient(composite, "sysmem"); |
| if (!sysmem_.is_valid()) { |
| DECODE_ERROR("Could not get SYSMEM protocol"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| canvas_ = ddk::AmlogicCanvasProtocolClient(composite, "canvas"); |
| if (!canvas_.is_valid()) { |
| DECODE_ERROR("Could not get video CANVAS protocol"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| clocks_[static_cast<int>(ClockType::kGclkVdec)] = |
| ddk::ClockProtocolClient(composite, "clock-dos-vdec"); |
| if (!clocks_[static_cast<int>(ClockType::kGclkVdec)].is_valid()) { |
| DECODE_ERROR("Could not get CLOCK protocol\n"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| clocks_[static_cast<int>(ClockType::kClkDos)] = ddk::ClockProtocolClient(composite, "clock-dos"); |
| if (!clocks_[static_cast<int>(ClockType::kClkDos)].is_valid()) { |
| DECODE_ERROR("Could not get CLOCK protocol\n"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| // If tee is available as a fragment, we require that we can get ZX_PROTOCOL_TEE. It'd be nice |
| // if there were a less fragile way to detect this. Passing in driver metadata for this doesn't |
| // seem worthwhile so far. There's no tee on vim2. |
| tee_ = ddk::TeeProtocolClient(composite, "tee"); |
| is_tee_available_ = tee_.is_valid(); |
| |
| if (is_tee_available_) { |
| // TODO(fxbug.dev/39808): remove log spam once we're loading firmware via video_firmware TA |
| LOG(INFO, "Got ZX_PROTOCOL_TEE"); |
| } else { |
| // TODO(fxbug.dev/39808): remove log spam once we're loading firmware via video_firmware TA |
| LOG(INFO, "Skipped ZX_PROTOCOL_TEE"); |
| } |
| |
| pdev_device_info_t info; |
| zx_status_t status = pdev_.GetDeviceInfo(&info); |
| if (status != ZX_OK) { |
| DECODE_ERROR("pdev_.GetDeviceInfo failed"); |
| return status; |
| } |
| |
| switch (info.pid) { |
| case PDEV_PID_AMLOGIC_S912: |
| device_type_ = DeviceType::kGXM; |
| break; |
| case PDEV_PID_AMLOGIC_S905D2: |
| device_type_ = DeviceType::kG12A; |
| break; |
| case PDEV_PID_AMLOGIC_T931: |
| device_type_ = DeviceType::kG12B; |
| break; |
| default: |
| DECODE_ERROR("Unknown soc pid: %d", info.pid); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| static constexpr uint32_t kTrustedOsSmcIndex = 0; |
| status = pdev_.GetSmc(kTrustedOsSmcIndex, &secure_monitor_); |
| if (status != ZX_OK) { |
| // On systems where there's no protected memory it's fine if we can't get |
| // a handle to the secure monitor. |
| LOG(INFO, "amlogic-video: Unable to get secure monitor handle, assuming no protected memory"); |
| } |
| |
| std::optional<ddk::MmioBuffer> cbus_mmio; |
| status = pdev_.MapMmio(kCbus, &cbus_mmio); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed map cbus"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| std::optional<ddk::MmioBuffer> mmio; |
| status = pdev_.MapMmio(kDosbus, &mmio); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed map dosbus"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| dosbus_.emplace(*std::move(mmio)); |
| status = pdev_.MapMmio(kHiubus, &mmio); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed map hiubus"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| hiubus_.emplace(*std::move(mmio)); |
| status = pdev_.MapMmio(kAobus, &mmio); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed map aobus"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| aobus_.emplace(*std::move(mmio)); |
| status = pdev_.MapMmio(kDmc, &mmio); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed map dmc"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| dmc_.emplace(*std::move(mmio)); |
| status = pdev_.GetInterrupt(kParserIrq, 0, &parser_interrupt_handle_); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed get parser interrupt"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| status = pdev_.GetInterrupt(kDosMbox0Irq, 0, &vdec0_interrupt_handle_); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed get vdec0 interrupt"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| status = pdev_.GetInterrupt(kDosMbox1Irq, 0, &vdec1_interrupt_handle_); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed get vdec interrupt"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| status = pdev_.GetBti(0, &bti_); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed get bti"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| int64_t reset_register_offset = 0x1100 * 4; |
| int64_t parser_register_offset = 0; |
| int64_t demux_register_offset = 0; |
| if (IsDeviceAtLeast(device_type_, DeviceType::kG12A)) { |
| // Some portions of the cbus moved in newer versions (TXL and later). |
| reset_register_offset = 0x0400 * 4; |
| parser_register_offset = (0x3800 - 0x2900) * 4; |
| demux_register_offset = (0x1800 - 0x1600) * 4; |
| } |
| reset_.emplace(*cbus_mmio, reset_register_offset); |
| parser_regs_.emplace(*cbus_mmio, parser_register_offset); |
| demux_.emplace(*cbus_mmio, demux_register_offset); |
| cbus_.emplace(*std::move(cbus_mmio)); |
| registers_ = std::unique_ptr<MmioRegisters>(new MmioRegisters{ |
| &*dosbus_, &*aobus_, &*dmc_, &*hiubus_, &*reset_, &*parser_regs_, &*demux_}); |
| |
| firmware_ = std::make_unique<FirmwareBlob>(); |
| status = firmware_->LoadFirmware(parent_); |
| if (status != ZX_OK) { |
| DECODE_ERROR("Failed load firmware"); |
| return status; |
| } |
| |
| sysmem_sync_ptr_.Bind(ConnectToSysmem()); |
| if (!sysmem_sync_ptr_) { |
| DECODE_ERROR("ConnectToSysmem() failed"); |
| status = ZX_ERR_INTERNAL; |
| return status; |
| } |
| parser_ = std::make_unique<Parser>(this, std::move(parser_interrupt_handle_)); |
| |
| if (is_tee_available()) { |
| // TODO(fxbug.dev/44674): Remove this retry loop once this issue is resolved. |
| constexpr uint32_t kRetryCount = 10; |
| for (uint32_t i = 0; i < kRetryCount; i++) { |
| status = EnsureSecmemSessionIsConnected(); |
| if (status == ZX_OK) { |
| break; |
| } |
| } |
| |
| if (!secmem_session_.has_value()) { |
| LOG(ERROR, |
| "OpenSession to secmem failed too many times. Bootloader version may be incorrect."); |
| return ZX_ERR_INTERNAL; |
| } |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlogicVideo::PreloadFirmwareViaTee() { |
| ZX_DEBUG_ASSERT(is_tee_available_); |
| |
| uint8_t* firmware_data; |
| uint32_t firmware_size; |
| firmware_->GetWholeBlob(&firmware_data, &firmware_size); |
| |
| // TODO(fxbug.dev/44764): Remove retry when video_firmware crash is fixed. |
| zx_status_t status = ZX_OK; |
| constexpr uint32_t kRetryCount = 10; |
| for (uint32_t i = 0; i < kRetryCount; i++) { |
| fuchsia::tee::DeviceSyncPtr tee_connection; |
| status = ConnectToTee(&tee_connection); |
| if (status != ZX_OK) { |
| LOG(ERROR, "ConnectToTee() failed - status: %d", status); |
| continue; |
| } |
| |
| auto video_firmware_session_result = VideoFirmwareSession::TryOpen(std::move(tee_connection)); |
| if (!video_firmware_session_result.is_ok()) { |
| // Logging handled in `VideoFirmwareSession::TryOpen` |
| status = ZX_ERR_INTERNAL; |
| continue; |
| } |
| |
| VideoFirmwareSession video_firmware_session = video_firmware_session_result.take_value(); |
| status = video_firmware_session.LoadVideoFirmware(firmware_data, firmware_size); |
| if (status != ZX_OK) { |
| LOG(ERROR, "video_firmware_session.LoadVideoFirmware() failed - status: %d", status); |
| continue; |
| } |
| |
| LOG(INFO, "Firmware loaded via video_firmware TA"); |
| break; |
| } |
| |
| return status; |
| } |
| |
| void AmlogicVideo::InitializeInterrupts() { |
| vdec0_interrupt_thread_ = std::thread([this]() { |
| while (true) { |
| zx_time_t time; |
| zx_status_t status = zx_interrupt_wait(vdec0_interrupt_handle_.get(), &time); |
| if (status != ZX_OK) { |
| DECODE_ERROR("vdec0_interrupt_thread_ zx_interrupt_wait() failed - status: %d", status); |
| return; |
| } |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| if (video_decoder_) { |
| video_decoder_->HandleInterrupt(); |
| } |
| } |
| }); |
| |
| vdec1_interrupt_thread_ = std::thread([this]() { |
| while (true) { |
| zx_time_t time; |
| zx_status_t status = zx_interrupt_wait(vdec1_interrupt_handle_.get(), &time); |
| if (status == ZX_ERR_CANCELED) { |
| // expected when zx_interrupt_destroy() is called |
| return; |
| } |
| if (status != ZX_OK) { |
| // unexpected errors |
| DECODE_ERROR( |
| "AmlogicVideo::InitializeInterrupts() zx_interrupt_wait() failed " |
| "status: %d\n", |
| status); |
| return; |
| } |
| std::lock_guard<std::mutex> lock(video_decoder_lock_); |
| if (video_decoder_) { |
| video_decoder_->HandleInterrupt(); |
| } |
| } |
| }); |
| } |
| |
| zx_status_t AmlogicVideo::InitDecoder() { |
| if (is_tee_available_) { |
| zx_status_t status = PreloadFirmwareViaTee(); |
| if (status != ZX_OK) { |
| is_tee_available_ = false; |
| // TODO(jbauman): Fail this function when everyone's updated their bootloaders. |
| LOG(INFO, "Preloading firmware failed with status %d. protected decode won't work.", status); |
| } else { |
| // TODO(dustingreen): Remove log spam after secure decode works. |
| LOG(INFO, "PreloadFirmwareViaTee() succeeded."); |
| } |
| } else { |
| LOG(INFO, "!is_tee_available_"); |
| } |
| |
| InitializeInterrupts(); |
| |
| return ZX_OK; |
| } |