blob: 16d763625138837e5609823b652136c060a9d679 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "codec_adapter_h264_multi.h"
#include <lib/fidl/cpp/clone.h>
#include <lib/fit/defer.h>
#include <lib/sync/completion.h>
#include <lib/trace/event.h>
#include <lib/zx/bti.h>
#include <zircon/assert.h>
#include <zircon/threads.h>
#include <zircon/time.h>
#include <limits>
#include <mutex>
#include <optional>
#include <bind/fuchsia/amlogic/platform/sysmem/heap/cpp/bind.h>
#include <bind/fuchsia/sysmem/heap/cpp/bind.h>
#include <fbl/algorithm.h>
#include <src/lib/memory_barriers/memory_barriers.h>
#include "device_ctx.h"
#include "h264_multi_decoder.h"
#include "macros.h"
#include "pts_manager.h"
#include "vdec1.h"
namespace amlogic_decoder {
namespace {
static inline constexpr uint32_t make_fourcc(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return (static_cast<uint32_t>(d) << 24) | (static_cast<uint32_t>(c) << 16) |
(static_cast<uint32_t>(b) << 8) | static_cast<uint32_t>(a);
}
template <typename T>
constexpr T AlignUpConstexpr(T value, T divisor) {
return (value + divisor - 1) / divisor * divisor;
}
// For experimentation purposes, this allows easily switching (with a local edit) to allowing larger
// input buffers, and using a larger stream buffer. When this is false, the input buffer max size
// and stream buffer size are tuned for decoding 1080p safely (barely).
constexpr bool k4kInputFrames = false;
// See VLD_PADDING_SIZE.
constexpr uint32_t kPaddingSize = 1024;
// This should be enough space to hold all headers (such as SEI, SPS, PPS), plus the kPaddingSize
// padding the decoder adds after each header that is delivered in its own packet, before a max-size
// frame. This should be large enough to also hold any zero-padding in the input stream before a
// max-size frame (typically none).
constexpr uint32_t kBigHeadersBytes = 128 * 1024;
// This is enough to decode 4:2:0 1920x1080 with MinCR 2, assuming headers before the frame don't
// exceed 128KiB.
constexpr uint32_t k1080pMaxCompressedFrameSize = 1920 * 1080 * 3 / 2 / 2;
constexpr uint32_t kDci4kMaxCompressedFrameSize = 4096u * 2160 * 3 / 2 / 2;
constexpr uint32_t k1080pMaxCompressedFrameSizeIncludingHeaders =
k1080pMaxCompressedFrameSize + kBigHeadersBytes;
constexpr uint32_t kDci4kMaxCompressedFrameSizeIncludingHeaders =
kDci4kMaxCompressedFrameSize + kBigHeadersBytes;
constexpr uint32_t kMaxCompressedFrameSizeIncludingHeaders =
k4kInputFrames ? kDci4kMaxCompressedFrameSizeIncludingHeaders
: k1080pMaxCompressedFrameSizeIncludingHeaders;
constexpr uint32_t kStreamBufferReadAlignment = 512;
// It might be reasonable to remove this adjustment, given some experimentation to see if the
// kStreamBufferReadAlignment is sufficient on its own to make kStreamBufferSize work.
constexpr uint32_t kReadNotEqualWriteAdjustment = 1;
// The ZX_PAGE_SIZE alignment is just because we won't really allocate a partial page via sysmem
// anyway, so we may as well use the rest of the last needed page even if kStreamBufferReadAlignment
// might technically work.
//
// The first kPaddingSize is to be able to flush through a first frame. The second kPaddingSize is
// because the first kPaddingSize is still in the stream buffer at the time we're decoding the
// second frame, because the first frame ended just after the first frame's payload data as far as
// the HW is concerned (despite the need for padding to cause the first frame to complete).
constexpr uint32_t kStreamBufferSize =
AlignUpConstexpr(kMaxCompressedFrameSizeIncludingHeaders + 2 * kPaddingSize +
kStreamBufferReadAlignment + kReadNotEqualWriteAdjustment,
static_cast<uint32_t>(ZX_PAGE_SIZE));
static_assert(kStreamBufferSize % ZX_PAGE_SIZE == 0);
// For now we rely on a compressed input frame to be contained entirely in a single buffer. While
// this minimum size may work for some demo streams, for now clients are expected to set a larger
// min_buffer_size for input, in their BufferCollectionConstraints. A recommended expression for
// min_buffer_size is max_width * max_height * 3 / 2 / 2 + 128 * 1024. This recommended expression
// accounts for MinCR (see h264 spec) of 2 which is worst-case, and allows for SEI/SPS/PPS that's up
// to 128 KiB which is probably enough for those headers.
constexpr uint32_t kInputPerPacketBufferBytesMin = 512 * 1024;
const uint32_t kInputPerPacketBufferBytesMax = kMaxCompressedFrameSizeIncludingHeaders;
constexpr uint32_t kInputBufferCountForCodecMin = 1;
constexpr uint32_t kInputBufferCountForCodecMax = 64;
} // namespace
CodecAdapterH264Multi::CodecAdapterH264Multi(std::mutex& lock,
CodecAdapterEvents* codec_adapter_events,
DeviceCtx* device)
: AmlogicCodecAdapter(lock, codec_adapter_events),
device_(device),
video_(device_->video()),
core_loop_(&kAsyncLoopConfigNoAttachToCurrentThread),
shared_fidl_thread_closure_queue_(std::in_place,
device->driver()->shared_fidl_loop()->dispatcher(),
device->driver()->shared_fidl_thread()) {
ZX_DEBUG_ASSERT(device_);
ZX_DEBUG_ASSERT(video_);
ZX_DEBUG_ASSERT(secure_memory_mode_[kInputPort] == fuchsia::mediacodec::SecureMemoryMode::OFF);
ZX_DEBUG_ASSERT(secure_memory_mode_[kOutputPort] == fuchsia::mediacodec::SecureMemoryMode::OFF);
thrd_t thrd;
zx_status_t status;
status = core_loop_.StartThread("H264 Core loop", &thrd);
ZX_ASSERT(status == ZX_OK);
device_->SetThreadProfile(zx::unowned_thread(thrd_get_zx_handle(thrd)),
ThreadRole::kH264MultiCore);
status = resource_loop_.StartThread("Resource loop", &thrd);
ZX_ASSERT(status == ZX_OK);
device_->SetThreadProfile(zx::unowned_thread(thrd_get_zx_handle(thrd)),
ThreadRole::kH264MultiResource);
}
CodecAdapterH264Multi::~CodecAdapterH264Multi() {
// We need to delete the shared_fidl_thread_closure_queue_ on its dispatcher thread, per the
// rules of ~ClosureQueue.
sync_completion_t shared_fidl_finished;
auto run_on_shared_fidl = [this, &shared_fidl_finished] {
shared_fidl_thread_closure_queue_.reset();
sync_completion_signal(&shared_fidl_finished);
};
if (thrd_current() == device_->driver()->shared_fidl_thread()) {
run_on_shared_fidl();
} else {
shared_fidl_thread_closure_queue_->Enqueue(run_on_shared_fidl);
}
sync_completion_wait(&shared_fidl_finished, ZX_TIME_INFINITE);
core_loop_.Shutdown();
resource_loop_.Shutdown();
}
void CodecAdapterH264Multi::SetCodecDiagnostics(CodecDiagnostics* codec_diagnostics) {
codec_diagnostics_ = codec_diagnostics->CreateDriverCodec("H264");
}
std::optional<media_metrics::StreamProcessorEvents2MigratedMetricDimensionImplementation>
CodecAdapterH264Multi::CoreCodecMetricsImplementation() {
return media_metrics::
StreamProcessorEvents2MigratedMetricDimensionImplementation_AmlogicDecoderH264;
}
bool CodecAdapterH264Multi::IsCoreCodecRequiringOutputConfigForFormatDetection() { return false; }
bool CodecAdapterH264Multi::IsCoreCodecMappedBufferUseful(CodecPort port) {
if (port == kInputPort) {
// Returning true here essentially means that we may be able to make use of mapped buffers if
// they're possible. However if is_secure true, we won't get a mapping and we don't really need
// a mapping, other than for avcC. If avcC shows up on input, we'll fail then.
//
// TODO(https://fxbug.dev/42110593): Add the failure when avcC shows up when is_secure, as
// described above.
return true;
} else {
ZX_DEBUG_ASSERT(port == kOutputPort);
return false;
}
}
bool CodecAdapterH264Multi::IsCoreCodecHwBased(CodecPort port) { return true; }
zx::unowned_bti CodecAdapterH264Multi::CoreCodecBti() { return zx::unowned_bti(video_->bti()); }
void CodecAdapterH264Multi::CoreCodecInit(
const fuchsia::media::FormatDetails& initial_input_format_details) {
initial_input_format_details_ = fidl::Clone(initial_input_format_details);
latest_input_format_details_ = fidl::Clone(initial_input_format_details);
// TODO(dustingreen): We do most of the setup in CoreCodecStartStream()
// currently, but we should do more here and less there.
}
void CodecAdapterH264Multi::CoreCodecSetSecureMemoryMode(
CodecPort port, fuchsia::mediacodec::SecureMemoryMode secure_memory_mode) {
// TODO(https://fxbug.dev/42116143): Ideally a codec list from the main CodecFactory would avoid
// reporting support for secure output or input when !is_tee_available(), which likely will mean
// reporting that in list from driver's local codec factory up to main factory. The main
// CodecFactory could also avoid handing out a codec that can't do secure output / input when the
// TEE isn't available, so we wouldn't end up here.
if (secure_memory_mode != fuchsia::mediacodec::SecureMemoryMode::OFF &&
!video_->is_tee_available()) {
events_->onCoreCodecFailCodec(
"BUG 40198 - Codec factory should catch earlier when secure requested without TEE.");
return;
}
secure_memory_mode_[port] = secure_memory_mode;
}
void CodecAdapterH264Multi::OnFrameReady(std::shared_ptr<VideoFrame> frame) {
TRACE_DURATION("media", "CodecAdapterH264Multi::OnFrameReady", "index", frame->index);
output_stride_ = frame->stride;
const CodecBuffer* buffer = frame->codec_buffer;
ZX_DEBUG_ASSERT(buffer);
uint64_t total_size_bytes = frame->stride * frame->coded_height * 3 / 2;
if (total_size_bytes > std::numeric_limits<uint32_t>::max()) {
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
return;
}
// The Codec interface requires that emitted frames are cache clean. We invalidate without
// skipping over stride-width per line, at least partly because stride - width is small (possibly
// always 0) for this decoder. But we do invalidate the UV section separately in case
// uv_plane_offset happens to leave significant space after the Y section (regardless of whether
// there's actually ever much padding there).
//
// TODO(dustingreen): Probably there's not ever any significant
// padding between Y and UV for this decoder, so probably can make one
// invalidate call here instead of two with no downsides.
// TODO(jbauman): avoid unnecessary cache ops when in RAM domain or when the buffer isn't
// mappable.
{
TRACE_DURATION("media", "cache invalidate");
if (!IsOutputSecure()) {
buffer->CacheFlushAndInvalidate(0, frame->stride * frame->coded_height);
buffer->CacheFlushAndInvalidate(frame->uv_plane_offset,
frame->stride * frame->coded_height / 2);
}
}
// We intentionally _don't_ use the packet with same index as the buffer (in
// general - it's fine that they sometimes match), to avoid clients building
// up inappropriate dependency on buffer index being the same as packet
// index (as nice as that would be, VP9, and maybe others, don't get along
// with that in general, so ... force clients to treat packet index and
// buffer index as separate things).
//
// Associate buffer with packet while the packet is in-flight.
CodecPacket* packet = GetFreePacket(buffer);
// With h.264, we know that an emitted buffer implies an available output
// packet, because h.264 doesn't put the same output buffer in flight more
// than once concurrently, and we have as many output packets as buffers.
// This contrasts with VP9 which has unbounded show_existing_frame.
ZX_DEBUG_ASSERT(packet);
packet->SetStartOffset(0);
packet->SetValidLengthBytes(static_cast<uint32_t>(total_size_bytes));
if (frame->has_pts) {
packet->SetTimstampIsh(frame->pts);
} else {
packet->ClearTimestampIsh();
}
events_->onCoreCodecOutputPacket(packet, false, false);
}
void CodecAdapterH264Multi::OnError() {
LOG(ERROR, "OnError()");
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
}
// TODO(dustingreen): A lot of the stuff created in this method should be able
// to get re-used from stream to stream. We'll probably want to factor out
// create/init from stream init further down.
void CodecAdapterH264Multi::CoreCodecStartStream() {
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
is_input_format_details_pending_ = true;
// At least until proven otherwise.
is_avcc_ = false;
is_input_end_of_stream_queued_ = false;
is_stream_failed_ = false;
} // ~lock
// Encapsulate stream buffer allocation in closure so that it can be posted on the resource thread
auto resource_init_function = fit::closure([this] { // scope lock
TRACE_DURATION("media", "Decoder Initialization");
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
// The output port is the one we really care about for is_secure of the decoder, since the HW
// can read from secure or non-secure even when in secure mode, but can only write to secure
// memory when in secure mode.
//
// Must create under lock to ensure that a potential other instance that incremented power
// ref(s) first is fully done un-gating clocks.
auto decoder = std::make_unique<H264MultiDecoder>(
video_, this, this, std::move(decoder_internal_buffers_), IsOutputSecure());
if (codec_diagnostics_) {
decoder->SetCodecDiagnostics(&codec_diagnostics_.value());
}
if (decoder->InitializeBuffers() != ZX_OK) {
events_->onCoreCodecFailCodec("InitializeBuffers() failed");
return;
}
decoder_ = decoder.get();
auto decoder_instance =
std::make_unique<DecoderInstance>(std::move(decoder), video_->vdec1_core());
StreamBuffer* buffer = decoder_instance->stream_buffer();
video_->AddNewDecoderInstance(std::move(decoder_instance));
std::optional<InternalBuffer> saved_stream_buffer = std::move(saved_stream_buffer_);
saved_stream_buffer_.reset();
if (video_->AllocateStreamBuffer(buffer, kStreamBufferSize, std::move(saved_stream_buffer),
/*use_parser=*/true, IsOutputSecure()) != ZX_OK) {
// Log here instead of in AllocateStreamBuffer() since video_ doesn't know which codec this is
// for.
events_->onCoreCodecLogEvent(
media_metrics::StreamProcessorEvents2MigratedMetricDimensionEvent::AllocationError);
events_->onCoreCodecFailCodec("AllocateStreamBuffer() failed");
return;
}
// ~lock
});
PostAndBlockResourceTask(std::move(resource_init_function));
}
void CodecAdapterH264Multi::CoreCodecQueueInputFormatDetails(
const fuchsia::media::FormatDetails& per_stream_override_format_details) {
// TODO(dustingreen): Consider letting the client specify profile/level info
// in the FormatDetails at least optionally, and possibly sizing input
// buffer constraints and/or other buffers based on that.
QueueInputItem(CodecInputItem::FormatDetails(per_stream_override_format_details));
}
void CodecAdapterH264Multi::CoreCodecQueueInputPacket(CodecPacket* packet) {
QueueInputItem(CodecInputItem::Packet(packet));
}
void CodecAdapterH264Multi::CoreCodecQueueInputEndOfStream() {
// This queues a marker, but doesn't force the HW to necessarily decode all
// the way up to the marker, depending on whether the client closes the stream
// or switches to a different stream first - in those cases it's fine for the
// marker to never show up as output EndOfStream.
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
is_input_end_of_stream_queued_ = true;
} // ~lock
QueueInputItem(CodecInputItem::EndOfStream());
}
// TODO(dustingreen): See comment on CoreCodecStartStream() re. not deleting
// creating as much stuff for each stream.
void CodecAdapterH264Multi::CoreCodecStopStream() {
std::list<CodecInputItem> leftover_input_items = CoreCodecStopStreamInternal();
for (auto& input_item : leftover_input_items) {
if (input_item.is_packet()) {
events_->onCoreCodecInputPacketDone(std::move(input_item.packet()));
}
}
}
// TODO(dustingreen): See comment on CoreCodecStartStream() re. not deleting
// creating as much stuff for each stream.
std::list<CodecInputItem> CodecAdapterH264Multi::CoreCodecStopStreamInternal() {
std::list<CodecInputItem> input_items_result;
{ // scope lock
std::unique_lock<std::mutex> lock(lock_);
bool is_cancelling_input_processing = true;
std::condition_variable stop_input_processing_condition;
async::PostTask(core_loop_.dispatcher(),
[this, &stop_input_processing_condition, &input_items_result,
&is_cancelling_input_processing] {
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
ZX_DEBUG_ASSERT(input_items_result.empty());
input_items_result.swap(input_queue_);
is_cancelling_input_processing = false;
} // ~lock
stop_input_processing_condition.notify_all();
});
while (is_cancelling_input_processing) {
stop_input_processing_condition.wait(lock);
}
ZX_DEBUG_ASSERT(!is_cancelling_input_processing);
} // ~lock
LOG(DEBUG, "RemoveDecoder()...");
auto resource_destroy_function = fit::closure([this] {
TRACE_DURATION("media", "Decoder Destruction");
std::lock_guard<std::mutex> decoder_lock(*video_->video_decoder_lock());
if (decoder_) {
video_->RemoveDecoderWithCallbackLocked(decoder_, [this](DecoderInstance* instance) {
// Stash the internal buffers so next decoder instance won't need to allocate new ones
// unless the buffers are the wrong size or wrong is_secure() etc. This saves time during
// seek and can save time during dimension changes when achieved by stream switching (in
// contrast to dimension changes achieved within a single stream, which don't remove the
// decoder during the dimension change when successful).
decoder_internal_buffers_.emplace(decoder_->TakeInternalBuffers());
ZX_DEBUG_ASSERT(!saved_stream_buffer_);
saved_stream_buffer_ = std::move(instance->stream_buffer()->optional_buffer());
});
decoder_ = nullptr;
}
});
PostAndBlockResourceTask(std::move(resource_destroy_function));
LOG(DEBUG, "RemoveDecoder() done.");
return input_items_result;
}
void CodecAdapterH264Multi::CoreCodecAddBuffer(CodecPort port, const CodecBuffer* buffer) {
if (port != kOutputPort) {
return;
}
ZX_DEBUG_ASSERT(port == kOutputPort);
// This flush is to eliminate any dirty cache lines. Our only choices are flush or flush and
// invalidate, so it's maybe slightly cheaper to only flush. We don't care what's being flushed
// here, if anything, since the buffer will be overwritten by HW decoding into the buffer anyway.
//
// There's a flush+invalidate later after the HW is done decoding, which we do for the invalidate
// part. For that flush to not overwrite anything the HW wrote to the buffer, this flush
// eliminates any dirty cache lines that might otherwise get flushed after HW has written to the
// buffer.
if (!IsOutputSecure()) {
ZX_DEBUG_ASSERT(buffer->size() <= std::numeric_limits<uint32_t>::max());
buffer->CacheFlush(0, static_cast<uint32_t>(buffer->size()));
}
all_output_buffers_.push_back(buffer);
}
void CodecAdapterH264Multi::CoreCodecConfigureBuffers(
CodecPort port, const std::vector<std::unique_ptr<CodecPacket>>& packets) {
if (port != kOutputPort) {
return;
}
ZX_DEBUG_ASSERT(port == kOutputPort);
// output
ZX_DEBUG_ASSERT(all_output_packets_.empty());
ZX_DEBUG_ASSERT(free_output_packets_.empty());
ZX_DEBUG_ASSERT(!all_output_buffers_.empty());
ZX_DEBUG_ASSERT(all_output_buffers_.size() <= packets.size());
for (auto& packet : packets) {
all_output_packets_.push_back(packet.get());
free_output_packets_.push_back(packet.get()->packet_index());
}
// This should prevent any inadvertent dependence by clients on the ordering
// of packet_index values in the output stream or any assumptions re. the
// relationship between packet_index and buffer_index.
std::shuffle(free_output_packets_.begin(), free_output_packets_.end(), not_for_security_prng_);
}
void CodecAdapterH264Multi::CoreCodecRecycleOutputPacket(CodecPacket* packet) {
if (packet->is_new()) {
packet->SetIsNew(false);
return;
}
ZX_DEBUG_ASSERT(!packet->is_new());
// A recycled packet will have a buffer set because the packet is in-flight
// until put on the free list, and has a buffer associated while in-flight.
const CodecBuffer* buffer = packet->buffer();
ZX_DEBUG_ASSERT(buffer);
// Eliminate any dirty CPU cache lines, so that later when we do a flush+invalidate after HW is
// done writing to the buffer, we won't be writing over anything the HW wrote.
if (!IsOutputSecure()) {
ZX_DEBUG_ASSERT(buffer->size() <= std::numeric_limits<uint32_t>::max());
buffer->CacheFlush(0, static_cast<uint32_t>(buffer->size()));
}
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
// Getting the buffer is all we needed the packet for. The packet won't get re-used until it
// goes back on the free list below.
//
// This must be done under lock_ to synchronize with InitializeFrames() with
// IsCurrentOutputBufferCollectionUsable() true. In particular we need to this synchronized to
// be able to tell InitializedFrames() exactly which CodecFrame(s) are initially free vs.
// initially used, based on which CodecFrame(s) correspond to a packet that is not free and has
// a specific buffer which corresponds to a not-free CodecFrame.
packet->SetBuffer(nullptr);
free_output_packets_.push_back(packet->packet_index());
} // ~lock
async::PostTask(core_loop_.dispatcher(), [this, video_frame = buffer->video_frame()]() {
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
std::shared_ptr<VideoFrame> frame = video_frame.lock();
if (!frame) {
// EndOfStream seen at the output, or a new InitializeFrames(), can cause
// !frame, which is fine. In that case, any new stream will request
// allocation of new frames.
return;
}
if (!decoder_)
return;
// Potentially this also pumps the decoder under video_decoder_lock.
decoder_->ReturnFrame(std::move(frame));
});
}
void CodecAdapterH264Multi::CoreCodecEnsureBuffersNotConfigured(CodecPort port) {
DLOG("port: %u", port);
std::lock_guard<std::mutex> lock(lock_);
// This adapter should ensure that zero old CodecPacket* or CodecBuffer*
// remain in this adapter (or below).
if (port == kInputPort) {
// There shouldn't be any queued input at this point, but if there is any,
// fail here even in a release build.
ZX_ASSERT(input_queue_.empty());
} else {
ZX_DEBUG_ASSERT(port == kOutputPort);
// The old all_output_buffers_ are no longer valid.
all_output_buffers_.clear();
all_output_packets_.clear();
free_output_packets_.clear();
output_buffer_collection_info_ = std::nullopt;
}
buffer_settings_[port] = std::nullopt;
}
std::unique_ptr<const fuchsia::media::StreamOutputConstraints>
CodecAdapterH264Multi::CoreCodecBuildNewOutputConstraints(
uint64_t stream_lifetime_ordinal, uint64_t new_output_buffer_constraints_version_ordinal,
bool buffer_constraints_action_required) {
// This decoder produces NV12.
auto config = std::make_unique<fuchsia::media::StreamOutputConstraints>();
config->set_stream_lifetime_ordinal(stream_lifetime_ordinal);
auto* constraints = config->mutable_buffer_constraints();
// For the moment, we always require buffer reallocation for any output constraints change.
ZX_DEBUG_ASSERT(buffer_constraints_action_required);
config->set_buffer_constraints_action_required(buffer_constraints_action_required);
constraints->set_buffer_constraints_version_ordinal(
new_output_buffer_constraints_version_ordinal);
// Ensure that if the client allocates its max + the server max that it won't go over the hardware
// limit (max_buffer_count).
if (max_buffer_count_[kOutputPort] <= min_buffer_count_[kOutputPort]) {
events_->onCoreCodecFailCodec("Impossible for client to satisfy buffer counts");
return nullptr;
}
return config;
}
fuchsia_sysmem2::BufferCollectionConstraints
CodecAdapterH264Multi::CoreCodecGetBufferCollectionConstraints2(
CodecPort port, const fuchsia::media::StreamBufferConstraints& stream_buffer_constraints,
const fuchsia::media::StreamBufferPartialSettings& partial_settings) {
fuchsia_sysmem2::BufferCollectionConstraints result;
// The CodecImpl won't hand us the sysmem token, so we shouldn't expect to
// have the token here.
ZX_DEBUG_ASSERT(!partial_settings.has_sysmem_token());
if (port == kInputPort) {
// We don't override CoreCodecBuildNewInputConstraints() for now, so pick these up from what was
// set by default implementation of CoreCodecBuildNewInputConstraints().
min_buffer_count_[kInputPort] = kInputBufferCountForCodecMin;
max_buffer_count_[kInputPort] = kInputBufferCountForCodecMax;
}
ZX_DEBUG_ASSERT(min_buffer_count_[port] != 0);
ZX_DEBUG_ASSERT(max_buffer_count_[port] != 0);
result.min_buffer_count_for_camping() = min_buffer_count_[port];
// Some slack is nice overall, but avoid having each participant ask for
// dedicated slack. Using sysmem the client will ask for it's own buffers for
// camping and any slack, so the codec doesn't need to ask for any extra on
// behalf of the client.
ZX_DEBUG_ASSERT(!result.min_buffer_count_for_dedicated_slack().has_value());
ZX_DEBUG_ASSERT(!result.min_buffer_count_for_shared_slack().has_value());
result.max_buffer_count() = max_buffer_count_[port];
uint32_t per_packet_buffer_bytes_min;
uint32_t per_packet_buffer_bytes_max;
if (port == kInputPort) {
per_packet_buffer_bytes_min = kInputPerPacketBufferBytesMin;
per_packet_buffer_bytes_max = kInputPerPacketBufferBytesMax;
} else {
ZX_DEBUG_ASSERT(port == kOutputPort);
// NV12, based on min stride.
per_packet_buffer_bytes_min = min_stride_ * height_ * 3 / 2;
// At least for now, don't cap the per-packet buffer size for output. The
// HW only cares about the portion we set up for output anyway, and the
// client has no way to force output to occur into portions of the output
// buffer beyond what's implied by the max supported image dimensions.
per_packet_buffer_bytes_max = 0xFFFFFFFF;
}
auto& bmc = result.buffer_memory_constraints().emplace();
bmc.min_size_bytes() = per_packet_buffer_bytes_min;
bmc.max_size_bytes() = per_packet_buffer_bytes_max;
// amlogic requires physically contiguous on both input and output
bmc.physically_contiguous_required() = true;
bmc.secure_required() = IsPortSecureRequired(port);
bmc.cpu_domain_supported() = !IsPortSecureRequired(port);
bmc.ram_domain_supported() = !IsPortSecureRequired(port) && (port == kOutputPort);
bmc.permitted_heaps().emplace();
if (IsPortSecurePermitted(port)) {
bmc.inaccessible_domain_supported() = true;
std::string secure_heap = (port == kInputPort)
? bind_fuchsia_amlogic_platform_sysmem_heap::HEAP_TYPE_SECURE_VDEC
: bind_fuchsia_amlogic_platform_sysmem_heap::HEAP_TYPE_SECURE;
fuchsia_sysmem2::Heap heap;
heap.heap_type() = std::move(secure_heap);
bmc.permitted_heaps()->emplace_back(std::move(heap));
}
if (!IsPortSecureRequired(port)) {
fuchsia_sysmem2::Heap heap;
heap.heap_type() = bind_fuchsia_sysmem_heap::HEAP_TYPE_SYSTEM_RAM;
bmc.permitted_heaps()->emplace_back(std::move(heap));
}
if (port == kOutputPort) {
auto& image_constraints = result.image_format_constraints().emplace().emplace_back();
image_constraints.pixel_format() = fuchsia_images2::PixelFormat::kNv12;
image_constraints.pixel_format_modifier() = fuchsia_images2::PixelFormatModifier::kLinear;
// TODO(https://fxbug.dev/42084950): confirm that REC709 is always what we want here, or plumb
// actual YUV color space if it can ever be REC601_*. Since 2020 and 2100
// are minimum 10 bits per Y sample and we're outputting NV12, 601 is the
// only other potential possibility here.
image_constraints.color_spaces() = {fuchsia_images2::ColorSpace::kRec709};
// The non-"required_" fields indicate the decoder's ability to potentially
// output frames at various dimensions as coded in the stream. Aside from
// the current stream being somewhere in these bounds, these have nothing to
// do with the current stream in particular.
image_constraints.min_size() = {16, 16};
// This intentionally isn't the _height_ of a 4096x2176 frame, it's
// intentionally the _width_ of a 4096x2176 frame assigned to
// max_coded_height.
//
// See max_coded_width_times_coded_height. We intentionally constrain the
// max dimension in width or height to the width of a 4096x2176 frame.
// While the HW might be able to go bigger than that as long as the other
// dimension is smaller to compensate, we don't really need to enable any
// larger than 4096x2176's width in either dimension, so we don't.
image_constraints.max_size() = {4096, 4096};
image_constraints.min_bytes_per_row() = 16;
// no hard-coded max stride, at least for now
image_constraints.max_bytes_per_row() = 0xFFFFFFFF;
image_constraints.max_width_times_height() = 4096 * 2176;
image_constraints.size_alignment() = {16, 16};
image_constraints.bytes_per_row_divisor() = H264MultiDecoder::kStrideAlignment;
// Even though we only ever output at offset 0, sysmem defaults start_offset_divisor to the
// image format alignment which is 2 for NV12. Since we are a producer, we should fully specify
// here so late attach clients don't have to specify it explicitly.
image_constraints.start_offset_divisor() = 2;
// Odd display dimensions are permitted, but these don't imply odd NV12
// dimensions - those are constrainted by coded_width_divisor and
// coded_height_divisor which are both 16.
image_constraints.display_rect_alignment() = {1, 1};
// The decoder is producing frames and the decoder has no choice but to
// produce frames at their coded size. The decoder wants to potentially be
// able to support a stream with dynamic resolution, potentially including
// dimensions both less than and greater than the dimensions that led to the
// current need to allocate a BufferCollection. For this reason, the
// required_ fields are set to the exact current dimensions, and the
// permitted (non-required_) fields is set to the full potential range that
// the decoder could potentially output. If an initiator wants to require a
// larger range of dimensions that includes the required range indicated
// here (via a-priori knowledge of the potential stream dimensions), an
// initiator is free to do so.
image_constraints.required_min_size() = {width_, height_};
image_constraints.required_max_size() = {width_, height_};
} else {
ZX_DEBUG_ASSERT(!result.image_format_constraints().has_value());
}
// We don't have to fill out usage - CodecImpl takes care of that.
ZX_DEBUG_ASSERT(!result.usage().has_value());
return result;
}
void CodecAdapterH264Multi::CoreCodecSetBufferCollectionInfo(
CodecPort port, const fuchsia_sysmem2::BufferCollectionInfo& buffer_collection_info) {
ZX_DEBUG_ASSERT(
*buffer_collection_info.settings()->buffer_settings()->is_physically_contiguous());
if (port == kOutputPort) {
ZX_DEBUG_ASSERT(buffer_collection_info.settings()->image_format_constraints().has_value());
ZX_DEBUG_ASSERT(
*buffer_collection_info.settings()->image_format_constraints()->pixel_format() ==
fuchsia_images2::PixelFormat::kNv12);
auto info_clone_result =
sysmem::V2CloneBufferCollectionInfo(buffer_collection_info, ZX_RIGHT_SAME_RIGHTS);
// failure to duplicate handles is treated similarly to OOM; fatal to this process
ZX_ASSERT(info_clone_result.is_ok());
output_buffer_collection_info_ = info_clone_result.take_value();
}
// clone / copy
buffer_settings_[port] = *buffer_collection_info.settings();
ZX_DEBUG_ASSERT(IsPortSecure(port) || !IsPortSecureRequired(port));
ZX_DEBUG_ASSERT(!IsPortSecure(port) || IsPortSecurePermitted(port));
// TODO(dustingreen): Remove after secure video decode works e2e.
LOG(DEBUG,
"CodecAdapterH264Multi::CoreCodecSetBufferCollectionInfo() - IsPortSecure(): %u port: %u",
IsPortSecure(port), port);
}
fuchsia::media::StreamOutputFormat CodecAdapterH264Multi::CoreCodecGetOutputFormat(
uint64_t stream_lifetime_ordinal, uint64_t new_output_format_details_version_ordinal) {
fuchsia::media::StreamOutputFormat result;
result.set_stream_lifetime_ordinal(stream_lifetime_ordinal);
result.mutable_format_details()->set_format_details_version_ordinal(
new_output_format_details_version_ordinal);
result.mutable_format_details()->set_mime_type("video/raw");
// For the moment, we'll memcpy to NV12 without any extra padding.
fuchsia::media::VideoUncompressedFormat video_uncompressed;
video_uncompressed.fourcc = make_fourcc('N', 'V', '1', '2');
video_uncompressed.primary_width_pixels = width_;
video_uncompressed.primary_height_pixels = height_;
video_uncompressed.secondary_width_pixels = width_ / 2;
video_uncompressed.secondary_height_pixels = height_ / 2;
// TODO(dustingreen): remove this field from the VideoUncompressedFormat or
// specify separately for primary / secondary.
video_uncompressed.planar = true;
video_uncompressed.swizzled = false;
video_uncompressed.primary_line_stride_bytes = output_stride_;
video_uncompressed.secondary_line_stride_bytes = output_stride_;
video_uncompressed.primary_start_offset = 0;
video_uncompressed.secondary_start_offset = output_stride_ * height_;
video_uncompressed.tertiary_start_offset = output_stride_ * height_ + 1;
video_uncompressed.primary_pixel_stride = 1;
video_uncompressed.secondary_pixel_stride = 2;
video_uncompressed.primary_display_width_pixels = display_width_;
video_uncompressed.primary_display_height_pixels = display_height_;
video_uncompressed.has_pixel_aspect_ratio = has_sar_;
video_uncompressed.pixel_aspect_ratio_width = sar_width_;
video_uncompressed.pixel_aspect_ratio_height = sar_height_;
// TODO(dustingreen): Deprecate and remove fields set above. Use only these fields (or newer
// variant of these fields; TBD):
video_uncompressed.image_format.pixel_format.type = fuchsia::sysmem::PixelFormatType::NV12;
video_uncompressed.image_format.coded_width = width_;
video_uncompressed.image_format.coded_height = height_;
DLOG("h264 coded_width: %u coded_height: %u", width_, height_);
video_uncompressed.image_format.bytes_per_row = output_stride_;
video_uncompressed.image_format.display_width = display_width_;
video_uncompressed.image_format.display_height = display_height_;
video_uncompressed.image_format.layers = 1;
video_uncompressed.image_format.color_space.type = fuchsia::sysmem::ColorSpaceType::REC709;
video_uncompressed.image_format.has_pixel_aspect_ratio = has_sar_;
video_uncompressed.image_format.pixel_aspect_ratio_width = sar_width_;
video_uncompressed.image_format.pixel_aspect_ratio_height = sar_height_;
fuchsia::media::VideoFormat video_format;
video_format.set_uncompressed(std::move(video_uncompressed));
result.mutable_format_details()->mutable_domain()->set_video(std::move(video_format));
return result;
}
void CodecAdapterH264Multi::CoreCodecMidStreamOutputBufferReConfigPrepare() {
// For this adapter, the core codec just needs us to get new frame buffers
// set up, so nothing to do here.
//
// CoreCodecEnsureBuffersNotConfigured() will run soon.
}
void CodecAdapterH264Multi::CoreCodecMidStreamOutputBufferReConfigFinish() {
MidStreamOutputBufferConfigInternal(true);
}
void CodecAdapterH264Multi::MidStreamOutputBufferConfigInternal(bool did_reallocate_buffers) {
// Now that the client has configured output buffers, we need to hand those
// back to the core codec via InitializedFrames.
std::vector<CodecFrame> frames;
uint32_t width;
uint32_t height;
uint32_t stride;
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
// Now we need to populate the frames_out vector.
for (uint32_t i = 0; i < all_output_buffers_.size(); i++) {
ZX_DEBUG_ASSERT(all_output_buffers_[i]->index() == i);
frames.emplace_back(*all_output_buffers_[i]);
frames.back().initial_usage_count() = 0;
}
for (auto* codec_packet : all_output_packets_) {
// The buffer() being non-null corresponds to the packet index not being in
// free_output_packets_. In other words, the non-null buffer() fields among all packets is
// all the used buffers. The buffer indexes and frames indexes are the same due to how frames
// is populated above.
if (codec_packet->buffer()) {
// This won't happen if we're doing a CoreCodecMidStreamOutputBufferReConfigFinish(). In
// that case we cleared all the packets and buffers and allocated new ones, so there won't
// be any packet with an assigned buffer, since there aren't any packets from before.
//
// On the other hand if we're telling an H264MultiDecoder about buffers that aren't new and
// may still be in flight on output, we some are not initially free ("initially" as in when
// InitializedFrames() is called).
//
// When !did_reallocate_buffers, we know that CoreCodecRecycleOutputPacket() won't be
// running for the entire MidStreamOutputBufferConfigInternal(). It's really that fact
// rather than the present lock_ interval that makes this initial_usage_count() stuff
// synchronize properly with CoreCodecRecycleOutputPacket().
ZX_DEBUG_ASSERT(!did_reallocate_buffers);
// h.264 doesn't have anything like vp9's show_existing_frame, so a given buffer is only
// downstream up to once at a time, so we know we won't see any CodecFrame/CodecBuffer
// that's currently referenced by more than one packet.
ZX_DEBUG_ASSERT(frames[codec_packet->buffer()->index()].initial_usage_count() == 0);
frames[codec_packet->buffer()->index()].initial_usage_count() = 1;
}
}
width = width_;
height = height_;
stride = fbl::round_up(width, *output_buffer_collection_info_->settings()
->image_format_constraints()
->bytes_per_row_divisor());
} // ~lock
auto resource_init_function =
fit::closure([this, width, height, stride, frames = std::move(frames)]() mutable {
TRACE_DURATION("media", "Decoder Frame Initialization");
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
decoder_->InitializedFrames(std::move(frames), width, height, stride);
});
// When we're really doing a mid-stream change, or when two consecutive streams are effectively
// part of an overall logical stream from the user's point of view (such as an upper-layer
// mid-video dimension change that ends up switching streams at this layer), posting over to a
// "resource" thread with different scheduler profile isn't really all that rigorous, since the
// resource setup aspects are inherently part of what needs to get done to achieve consistent
// output timing of decoded frames. But by doing this we can avoid needing to boost the scheduler
// profile budget for the current thread, at least for now (which again, isn't particularly
// rigorous, but it's why this is posting and immediately waiting). It's also relevant that the
// scheduler presently has an "anti-abuse" behavior where a thread gets de-scheduled each time it
// enables a deadline profile, so that's why we post-and-wait here instead of having the current
// thread switch its own scheduler deadline profile off/on. This is entirely about optimizing
// stream startup duration in the common case (which of course matters), not about rigor for
// scheduling aspects of mid-stream dimension change (which is fine for now, but may be improved).
PostAndBlockResourceTask(std::move(resource_init_function));
async::PostTask(core_loop_.dispatcher(), [this] {
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
if (!decoder_) {
return;
}
// Something else may have come along since InitializedFrames and pumped the decoder, but that's
// ok.
decoder_->PumpOrReschedule();
});
}
void CodecAdapterH264Multi::PostAndBlockResourceTask(fit::closure task_function) {
sync_completion_t resource_finished;
auto task =
fit::closure([&resource_finished, task_function = std::move(task_function)]() mutable {
task_function();
sync_completion_signal(&resource_finished);
});
zx_status_t task_result = async::PostTask(resource_loop_.dispatcher(), std::move(task));
if (task_result != ZX_OK) {
LOG(ERROR, "Could not post task to resource thread");
}
sync_completion_wait(&resource_finished, ZX_TIME_INFINITE);
}
void CodecAdapterH264Multi::QueueInputItem(CodecInputItem input_item, bool at_front) {
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
// For now we don't worry about avoiding a trigger if we happen to queue
// when ProcessInput() has removed the last item but ProcessInput() is still
// running.
if (at_front) {
input_queue_.emplace_front(std::move(input_item));
} else {
input_queue_.emplace_back(std::move(input_item));
}
if (!have_queued_trigger_decoder_) {
have_queued_trigger_decoder_ = true;
async::PostTask(core_loop_.dispatcher(), [this]() {
{
std::lock_guard<std::mutex> lock(lock_);
have_queued_trigger_decoder_ = false;
}
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
if (!decoder_)
return;
decoder_->ReceivedNewInput();
});
}
} // ~lock
}
CodecInputItem CodecAdapterH264Multi::DequeueInputItem() {
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
if (is_stream_failed_ || input_queue_.empty()) {
return CodecInputItem::Invalid();
}
CodecInputItem to_ret = std::move(input_queue_.front());
input_queue_.pop_front();
return to_ret;
} // ~lock
}
std::optional<H264MultiDecoder::DataInput> CodecAdapterH264Multi::ReadMoreInputData() {
H264MultiDecoder::DataInput result;
while (true) {
CodecInputItem item = DequeueInputItem();
if (!item.is_valid()) {
return std::nullopt;
}
if (item.is_format_details()) {
// TODO(dustingreen): Be more strict about what the input format actually
// is, and less strict about it matching the initial format.
ZX_ASSERT(fidl::Equals(item.format_details(), initial_input_format_details_));
latest_input_format_details_ = fidl::Clone(item.format_details());
is_input_format_details_pending_ = true;
continue;
}
if (item.is_end_of_stream()) {
result.is_eos = true;
is_input_end_of_stream_queued_to_core_ = true;
return result;
}
ZX_DEBUG_ASSERT(item.is_packet());
if (is_input_format_details_pending_) {
is_input_format_details_pending_ = false;
std::vector<uint8_t> oob_bytes = ParseCodecOobBytes();
if (!oob_bytes.empty()) {
result.length = oob_bytes.size();
result.data = std::move(oob_bytes);
// Put packet back for next call to ReadMoreInputData().
QueueInputItem(std::move(item), true);
return result;
}
}
fit::deferred_callback return_input_packet = fit::defer_callback(fit::closure(
[this, packet = item.packet()] { events_->onCoreCodecInputPacketDone(packet); }));
uint8_t* data = item.packet()->buffer()->base() + item.packet()->start_offset();
uint32_t len = item.packet()->valid_length_bytes();
auto parsed_input_data = ParseVideo(item.packet()->buffer(), &return_input_packet, data, len);
if (!parsed_input_data) {
continue;
}
result = std::move(parsed_input_data.value());
if (result.codec_buffer && !IsPortSecure(kInputPort)) {
// In case input is still dirty in CPU cache.
ZX_DEBUG_ASSERT(result.length <= std::numeric_limits<uint32_t>::max());
result.codec_buffer->CacheFlush(result.buffer_start_offset,
static_cast<uint32_t>(result.length));
}
if (item.packet()->has_timestamp_ish()) {
result.pts = item.packet()->timestamp_ish();
}
return result;
// ~item
}
}
bool CodecAdapterH264Multi::HasMoreInputData() {
std::lock_guard<std::mutex> lock(lock_);
return !input_queue_.empty();
}
std::vector<uint8_t> CodecAdapterH264Multi::ParseCodecOobBytes() {
// Our latest oob_bytes may contain SPS/PPS info. If we have any
// such info, the core codec needs it (possibly converted first).
// If there's no OOB info, then there's nothing to do, as all such info will
// be in-band in normal packet-based AnnexB NALs (including start codes and
// start code emulation prevention bytes).
if (!latest_input_format_details_.has_oob_bytes() ||
latest_input_format_details_.oob_bytes().empty()) {
// success
return {};
}
const std::vector<uint8_t>* oob = &latest_input_format_details_.oob_bytes();
// We need to deliver Annex B style SPS/PPS to this core codec, regardless of
// what format the oob_bytes is in.
// The oob_bytes can be in two different forms, which can be detected by
// the value of the first byte:
//
// 0 - Annex B form already. The 0 is the first byte of a start code.
// 1 - AVCC form, which we'll convert to Annex B form. AVCC version 1. There
// is no AVCC version 0.
// anything else - fail.
//
// In addition, we need to know if AVCC or not since we need to know whether
// to add start code emulation prevention bytes or not. And if it's AVCC,
// how many bytes long the pseudo_nal_length field is - that field is before
// each input NAL.
// We already checked empty() above.
ZX_DEBUG_ASSERT(oob->size() >= 1);
switch ((*oob)[0]) {
case 0:
is_avcc_ = false;
return *oob;
case 1: {
// This applies to both the oob data and the input packet payload data.
// Both are AVCC, or both are AnnexB.
is_avcc_ = true;
/*
AVCC OOB data layout (bits):
[0] (8) - version 1
[1] (8) - h264 profile #
[2] (8) - compatible profile bits
[3] (8) - h264 level (eg. 31 == "3.1")
[4] (6) - reserved, can be set to all 1s
(2) - pseudo_nal_length_field_bytes_ - 1
[5] (3) - reserved, can be set to all 1s
(5) - sps_count
(16) - sps_bytes
(8*sps_bytes) - SPS nal_unit_type (that byte) + SPS data as RBSP.
(8) - pps_count
(16) - pps_bytes
(8*pps_bytes) - PPS nal_unit_type (that byte) + PPS data as RBSP.
*/
// We accept 0 SPS and/or 0 PPS, but typically there's one of each. At
// minimum the oob buffer needs to be large enough to contain both the
// sps_count and pps_count fields, which is a min of 7 bytes.
if (oob->size() < 7) {
LOG(ERROR, "oob->size() < 7");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
// All pseudo-NALs in input packet payloads will use the
// parsed count of bytes of the length field. Convert SPS/PPS inline to AnnexB format so we
// can return it directly, as ParseVideo won't be called on this data.
pseudo_nal_length_field_bytes_ = ((*oob)[4] & 0x3) + 1;
uint32_t sps_count = (*oob)[5] & 0x1F;
uint32_t offset = 6;
std::vector<uint8_t> accumulation;
for (uint32_t i = 0; i < sps_count; ++i) {
if (offset + 2 > oob->size()) {
LOG(ERROR, "offset + 2 > oob->size()");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
uint32_t sps_length = (*oob)[offset] * 256 + (*oob)[offset + 1];
if (offset + 2 + sps_length > oob->size()) {
LOG(ERROR, "offset + 2 + sps_length > oob->size()");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
offset += 2; // sps_bytes
accumulation.push_back(0);
accumulation.push_back(0);
accumulation.push_back(0);
accumulation.push_back(1);
for (uint32_t i = 0; i < sps_length; i++) {
accumulation.push_back(oob->data()[offset + i]);
}
offset += sps_length;
}
if (offset + 1 > oob->size()) {
LOG(ERROR, "offset + 1 > oob->size()");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
uint32_t pps_count = (*oob)[offset++];
for (uint32_t i = 0; i < pps_count; ++i) {
if (offset + 2 > oob->size()) {
LOG(ERROR, "offset + 2 > oob->size()");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
uint32_t pps_length = (*oob)[offset] * 256 + (*oob)[offset + 1];
if (offset + 2 + pps_length > oob->size()) {
LOG(ERROR, "offset + 2 + pps_length > oob->size()");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
offset += 2; // pps_bytes
accumulation.push_back(0);
accumulation.push_back(0);
accumulation.push_back(0);
accumulation.push_back(1);
for (uint32_t i = 0; i < pps_length; i++) {
accumulation.push_back(oob->data()[offset + i]);
}
offset += pps_length;
}
return accumulation;
}
default:
LOG(ERROR, "unexpected first oob byte");
OnCoreCodecFailStream(fuchsia::media::StreamError::INVALID_INPUT_FORMAT_DETAILS);
return {};
}
}
std::optional<H264MultiDecoder::DataInput> CodecAdapterH264Multi::ParseVideo(
const CodecBuffer* buffer, fit::deferred_callback* return_input_packet, const uint8_t* data,
uint32_t length) {
if (is_avcc_) {
return ParseVideoAvcc(data, length);
// ~return_input_packet
} else {
return ParseVideoAnnexB(buffer, return_input_packet, data, length);
}
}
std::optional<H264MultiDecoder::DataInput> CodecAdapterH264Multi::ParseVideoAvcc(
const uint8_t* data, uint32_t length) {
// We don't necessarily know that is_avcc_ is true on entry to this method.
// We use this method to send the decoder a bunch of 0x00 sometimes, which
// will call this method regardless of is_avcc_ or not.
// So far, the "avcC"/"AVCC" we've seen has emulation prevention bytes on it
// already. So we don't add those here. But if we did need to add them, we'd
// add them here.
// For now we assume the heap is pretty fast and doesn't mind the size thrash,
// but maybe we'll want to keep a buffer around (we'll optimize only if/when
// we determine this is actually a problem). We only actually use this buffer
// if is_avcc_ (which is not uncommon).
// We do parse more than one pseudo_nal per input packet.
//
// No splitting NALs across input packets, for now.
//
// TODO(dustingreen): Allow splitting NALs across input packets (not a small
// change). Probably also move into a source_set for sharing with other
// CodecAdapter(s).
// Count the input pseudo_nal(s)
uint32_t pseudo_nal_count = 0;
uint32_t i = 0;
while (i < length) {
if (i + pseudo_nal_length_field_bytes_ > length) {
LOG(ERROR, "i + pseudo_nal_length_field_bytes_ > length");
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
return {};
}
// Read pseudo_nal_length field, which is a field which can be 1-4 bytes
// long because AVCC/avcC.
uint32_t pseudo_nal_length = 0;
for (uint32_t length_byte = 0; length_byte < pseudo_nal_length_field_bytes_; ++length_byte) {
pseudo_nal_length = pseudo_nal_length * 256 + data[i + length_byte];
}
i += pseudo_nal_length_field_bytes_;
if (i + pseudo_nal_length > length) {
LOG(ERROR, "i + pseudo_nal_length > length");
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
return {};
}
i += pseudo_nal_length;
++pseudo_nal_count;
}
static constexpr uint32_t kStartCodeBytes = 4;
uint32_t local_length = length - pseudo_nal_count * pseudo_nal_length_field_bytes_ +
pseudo_nal_count * kStartCodeBytes;
auto local_buffer = std::make_unique<uint8_t[]>(local_length);
uint8_t* local_data = local_buffer.get();
i = 0;
uint32_t o = 0;
while (i < length) {
if (i + pseudo_nal_length_field_bytes_ > length) {
LOG(ERROR, "i + pseudo_nal_length_field_bytes_ > length");
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
return {};
}
uint32_t pseudo_nal_length = 0;
for (uint32_t length_byte = 0; length_byte < pseudo_nal_length_field_bytes_; ++length_byte) {
pseudo_nal_length = pseudo_nal_length * 256 + data[i + length_byte];
}
i += pseudo_nal_length_field_bytes_;
if (i + pseudo_nal_length > length) {
LOG(ERROR, "i + pseudo_nal_length > length");
OnCoreCodecFailStream(fuchsia::media::StreamError::DECODER_UNKNOWN);
return {};
}
local_data[o++] = 0;
local_data[o++] = 0;
local_data[o++] = 0;
local_data[o++] = 1;
memcpy(&local_data[o], &data[i], pseudo_nal_length);
o += pseudo_nal_length;
i += pseudo_nal_length;
}
ZX_DEBUG_ASSERT(o == local_length);
ZX_DEBUG_ASSERT(i == length);
return ParseVideoAnnexB(nullptr, nullptr, local_data, local_length);
}
std::optional<H264MultiDecoder::DataInput> CodecAdapterH264Multi::ParseVideoAnnexB(
const CodecBuffer* buffer, fit::deferred_callback* return_input_packet, const uint8_t* data,
uint32_t length) {
ZX_DEBUG_ASSERT(data);
ZX_DEBUG_ASSERT(!!buffer == !!return_input_packet);
H264MultiDecoder::DataInput result;
result.length = length;
if (!buffer) {
result.data = std::vector<uint8_t>(data, data + length);
} else {
ZX_DEBUG_ASSERT(buffer);
// Caller is required to ensure that data is within [base()..base()+size()).
ZX_DEBUG_ASSERT(data >= buffer->base());
ZX_DEBUG_ASSERT(data < buffer->base() + buffer->size());
ZX_DEBUG_ASSERT(data - buffer->base() <= std::numeric_limits<uint32_t>::max());
ZX_DEBUG_ASSERT(return_input_packet);
result.codec_buffer = buffer;
result.buffer_start_offset = static_cast<uint32_t>(data - buffer->base());
result.return_input_packet = std::move(*return_input_packet);
}
return std::move(result);
}
void CodecAdapterH264Multi::OnEos() { events_->onCoreCodecOutputEndOfStream(false); }
zx_status_t CodecAdapterH264Multi::InitializeFrames(uint32_t min_frame_count,
uint32_t max_frame_count, uint32_t coded_width,
uint32_t coded_height, uint32_t stride,
uint32_t display_width, uint32_t display_height,
bool has_sar, uint32_t sar_width,
uint32_t sar_height) {
// This is called on a core codec thread, ordered with respect to emitted
// output frames.
//
// Frame initialization is async.
//
// If existing buffers are suitable, we can init / re-init frames without
// re-allocating buffers, so we don't need the client's help (or sysmem's),
// and it's faster.
//
// If existing buffers are not suitable, this completes when either the client
// has configured output buffers and we've done core codec
// InitializedFrames(), or until the cilent has moved on by closing the
// current stream.
//
// The video_decoder_lock_ is held during this method.
//
// First stash some format and buffer count info needed to initialize frames
// before triggering re-init of frames / mid-stream format change. Later,
// frames satisfying these stashed parameters will be handed to the decoder
// via InitializedFrames(), unless CoreCodecStopStream() happens first.
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
min_buffer_count_[kOutputPort] = min_frame_count;
max_buffer_count_[kOutputPort] = max_frame_count;
width_ = coded_width;
height_ = coded_height;
min_stride_ = stride;
display_width_ = display_width;
display_height_ = display_height;
has_sar_ = has_sar;
sar_width_ = sar_width;
sar_height_ = sar_height;
} // ~lock
// After a stream switch, the new H264MultiDecoder won't have any frames, and needs
// InitializedFrames() to get called to set up the frames, and won't have checked
// IsCurrentOutputBufferCollectionUsable() itself since it knows it needs frames configured using
// InitializedFrames(). However, we can still check here whether the current buffer collection,
// that was (before the stream switch) used with a previous H264MultiDecoder instance, can still
// be used with the new H264MultiDecoder instance. This does require that we be able to indicate
// via InitializedFrames() which frames are presently usable vs. which are still downstream and
// not yet returned.
if (IsCurrentOutputBufferCollectionUsable(min_frame_count, max_frame_count, coded_width,
coded_height, stride, display_width, display_height)) {
DLOG("IsCurrentOutputBufferCollectionUsable() true");
// The core codec won't output any more packets until we call InitializedFrames(), but when the
// core codec does output more packets, we need to send updated format info first.
//
// TODO(dustingreen): This may be unnecessary / redundant.
events_->onCoreCodecOutputFormatChange();
shared_fidl_thread_closure_queue_->Enqueue([this] {
// We have to run this on the shared fidl thread since that's what CodecImpl is using to
// process RecycleOutputPacket(); we need to avoid this running concurrently with
// CoreCodecRecycleOutputPacket().
MidStreamOutputBufferConfigInternal(false);
});
return ZX_OK;
}
// If we don't have a current output BufferCollection or can't re-use it due to unsuitable
// constraints, we need to trigger a mid-stream output constraints change to trigger a new
// BufferCollection to be allocated that's consistent with the new constraints.
//
// This will snap the current stream_lifetime_ordinal_, and call
// CoreCodecMidStreamOutputBufferReConfigPrepare() and
// CoreCodecMidStreamOutputBufferReConfigFinish() from the StreamControl
// thread, _iff_ the client hasn't already moved on to a new stream by then.
events_->onCoreCodecMidStreamOutputConstraintsChange(true);
return ZX_OK;
}
bool CodecAdapterH264Multi::IsCurrentOutputBufferCollectionUsable(
uint32_t min_frame_count, uint32_t max_frame_count, uint32_t coded_width, uint32_t coded_height,
uint32_t stride, uint32_t display_width, uint32_t display_height) {
DLOG(
"min_frame_count: %u max_frame_count: %u coded_width: %u coded_height: %u stride: %u "
"display_width: %u display_height: %u",
min_frame_count, max_frame_count, coded_width, coded_height, stride, display_width,
display_height);
ZX_DEBUG_ASSERT(stride >= coded_width);
// We don't ask codec_impl about this, because as far as codec_impl is
// concerned, the output buffer collection might not be used for video
// frames. We could have common code for video decoders but for now we just
// check here.
//
// TODO(dustingreen): Some potential divisor check failures could be avoided
// if the corresponding value were rounded up according to the divisor before
// we get here.
if (!output_buffer_collection_info_) {
LOG(DEBUG, "!output_buffer_collection_info_");
return false;
}
fuchsia_sysmem2::BufferCollectionInfo& info = output_buffer_collection_info_.value();
ZX_DEBUG_ASSERT(info.settings()->image_format_constraints().has_value());
if (min_frame_count > info.buffers()->size()) {
LOG(DEBUG, "min_frame_count > info.buffers().size()");
return false;
}
if (min_frame_count > min_buffer_count_[kOutputPort]) {
LOG(DEBUG, "min_frame_count > min_buffer_count_[kOutputPort]");
return false;
}
if (info.buffers()->size() > max_frame_count) {
// The h264_multi_decoder.cc won't exercise this path since the max is always the same, and we
// won't have allocated a collection with more than max_buffer_count.
LOG(DEBUG, "info.buffer_count > max_frame_count");
return false;
}
if (stride * coded_height * 3 / 2 > *info.settings()->buffer_settings()->size_bytes()) {
LOG(DEBUG, "stride * coded_height * 3 / 2 > info.settings.buffer_settings.size_bytes");
return false;
}
if (display_width %
info.settings()->image_format_constraints()->display_rect_alignment()->width() !=
0) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG,
"display_width %% info.settings.image_format_constraints.display_width_divisor != 0");
return false;
}
if (display_height %
info.settings()->image_format_constraints()->display_rect_alignment()->height() !=
0) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG,
"display_height %% info.settings.image_format_constraints.display_height_divisor != 0");
return false;
}
if (coded_width * coded_height >
*info.settings()->image_format_constraints()->max_width_times_height()) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG, "coded_width * coded_height > max_coded_width_times_coded_height");
return false;
}
if (coded_width < info.settings()->image_format_constraints()->min_size()->width()) {
LOG(DEBUG,
"coded_width < info.settings.image_format_constraints.min_coded_width -- "
"coded_width: %d min_coded_width: %d",
coded_width, info.settings()->image_format_constraints()->min_size()->width());
return false;
}
if (coded_width > info.settings()->image_format_constraints()->max_size()->width()) {
LOG(DEBUG, "coded_width > info.settings.image_format_constraints.max_coded_width");
return false;
}
if (coded_width % info.settings()->image_format_constraints()->size_alignment()->width() != 0) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG, "coded_width %% info.settings.image_format_constraints.coded_width_divisor != 0");
return false;
}
if (coded_height < info.settings()->image_format_constraints()->min_size()->height()) {
LOG(DEBUG, "coded_height < info.settings.image_format_constraints.min_coded_height");
return false;
}
if (coded_height > info.settings()->image_format_constraints()->max_size()->height()) {
LOG(DEBUG, "coded_height > info.settings.image_format_constraints.max_coded_height");
return false;
}
if (coded_height % info.settings()->image_format_constraints()->size_alignment()->height() != 0) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG, "coded_height %% info.settings.image_format_constraints.coded_height_divisor != 0");
return false;
}
if (stride < *info.settings()->image_format_constraints()->min_bytes_per_row()) {
LOG(DEBUG,
"stride < info.settings.image_format_constraints.min_bytes_per_row -- stride: %d "
"min_bytes_per_row: %d",
stride, *info.settings()->image_format_constraints()->min_bytes_per_row());
return false;
}
if (stride > *info.settings()->image_format_constraints()->max_bytes_per_row()) {
LOG(DEBUG, "stride > info.settings.image_format_constraints.max_bytes_per_row");
return false;
}
if (stride % *info.settings()->image_format_constraints()->bytes_per_row_divisor() != 0) {
// Let it probably fail later when trying to re-negotiate buffers.
LOG(DEBUG, "stride %% info.settings.image_format_constraints.bytes_per_row_divisor != 0");
return false;
}
DLOG("returning true");
return true;
}
void CodecAdapterH264Multi::AsyncPumpDecoder() {
async::PostTask(core_loop_.dispatcher(), [this] {
std::lock_guard<std::mutex> lock(*video_->video_decoder_lock());
if (!decoder_)
return;
// Something else may have come along since InitializedFrames and pumped the decoder, but that's
// ok.
decoder_->PumpOrReschedule();
});
}
void CodecAdapterH264Multi::AsyncResetStreamAfterCurrentFrame() {
LOG(ERROR, "async reset stream (after current frame) triggered");
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
// The current stream is temporarily failed, until CoreCodecResetStreamAfterCurrentFrame() soon
// on the StreamControl thread. This prevents ReadMoreInputData() from queueing any more input
// data after any currently-running iteration.
//
// While Vp9Decoder::needs_more_input_data() may already be returning false which may serve a
// similar purpose depending on how/when Vp9Decoder calls this method, it's nice to directly
// mute queing any more input in this layer.
is_stream_failed_ = true;
} // ~lock
events_->onCoreCodecResetStreamAfterCurrentFrame();
}
void CodecAdapterH264Multi::CoreCodecResetStreamAfterCurrentFrame() {
// Currently this takes ~20-40ms per reset. We might be able to improve the performance by having
// a stop that doesn't deallocate followed by a start that doesn't allocate, but since we'll
// fairly soon only be using this method during watchdog processing, it's not worth optimizing for
// the temporary time interval during which we might potentially use this on multiple
// non-keyframes in a row before a keyframe, only in the case of protected input.
//
// If we were to optimize in that way, it'd increase the complexity of init and de-init code. The
// current way we use that code exactly the same way for reset as for init and de-init, which is
// good from a test coverage point of view.
// This fences and quiesces the input processing thread, and the StreamControl thread (current
// thread) is the only other thread that modifies is_input_end_of_stream_queued_to_core_, so we
// know is_input_end_of_stream_queued_to_core_ won't be changing.
LOG(DEBUG, "before CoreCodecStopStreamInternal()");
std::list<CodecInputItem> input_items = CoreCodecStopStreamInternal();
auto return_any_input_items = fit::defer([this, &input_items] {
for (auto& input_item : input_items) {
if (input_item.is_packet()) {
events_->onCoreCodecInputPacketDone(std::move(input_item.packet()));
}
}
});
if (is_input_end_of_stream_queued_to_core_) {
// We don't handle this corner case of a corner case. Fail the stream instead.
events_->onCoreCodecFailStream(fuchsia::media::StreamError::EOS_PROCESSING);
return;
}
LOG(DEBUG, "after stop; before CoreCodecStartStream()");
CoreCodecStartStream();
LOG(DEBUG, "re-queueing items...");
while (!input_items.empty()) {
QueueInputItem(std::move(input_items.front()));
input_items.pop_front();
}
LOG(DEBUG, "done re-queueing items.");
}
void CodecAdapterH264Multi::CoreCodecSetStreamControlProfile(
zx::unowned_thread stream_control_thread) {
device_->SetThreadProfile(std::move(stream_control_thread), ThreadRole::kH264MultiStreamControl);
}
void CodecAdapterH264Multi::OnCoreCodecFailStream(fuchsia::media::StreamError error) {
{ // scope lock
std::lock_guard<std::mutex> lock(lock_);
is_stream_failed_ = true;
}
LOG(INFO, "calling events_->onCoreCodecFailStream(): %u", static_cast<uint32_t>(error));
events_->onCoreCodecFailStream(error);
}
CodecPacket* CodecAdapterH264Multi::GetFreePacket(const CodecBuffer* buffer) {
std::lock_guard<std::mutex> lock(lock_);
// The h264 decoder won't repeatedly output a buffer multiple times
// concurrently, so a free buffer (for which the caller needs a packet)
// implies a free packet.
ZX_DEBUG_ASSERT(!free_output_packets_.empty());
uint32_t free_index = free_output_packets_.back();
free_output_packets_.pop_back();
CodecPacket* packet = all_output_packets_[free_index];
// Associate the buffer with the packet while the packet is in-flight. We don't strictly need to
// be doing this under lock_, but doesn't hurt, and it's easier to understand how things work with
// this under lock_.
packet->SetBuffer(buffer);
return packet;
}
bool CodecAdapterH264Multi::IsPortSecureRequired(CodecPort port) {
return secure_memory_mode_[port] == fuchsia::mediacodec::SecureMemoryMode::ON;
}
bool CodecAdapterH264Multi::IsPortSecurePermitted(CodecPort port) {
return secure_memory_mode_[port] != fuchsia::mediacodec::SecureMemoryMode::OFF;
}
bool CodecAdapterH264Multi::IsPortSecure(CodecPort port) {
ZX_DEBUG_ASSERT(buffer_settings_[port]);
return *buffer_settings_[port]->buffer_settings()->is_secure();
}
bool CodecAdapterH264Multi::IsOutputSecure() {
// We need to know whether output is secure or not before we start accepting input, which means
// we need to know before output buffers are allocated, which means we can't rely on the result
// of sysmem BufferCollection allocation is_secure for output.
ZX_DEBUG_ASSERT(IsPortSecurePermitted(kOutputPort) == IsPortSecureRequired(kOutputPort));
return IsPortSecureRequired(kOutputPort);
}
} // namespace amlogic_decoder