blob: 3e98dc06cdf993aff43b133e6480c187625ff495 [file] [log] [blame]
// Copyright 2019 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 "src/camera/drivers/controller/pipeline_manager.h"
#include <fidl/fuchsia.sysmem/cpp/hlcpp_conversion.h>
#include <fidl/fuchsia.sysmem2/cpp/hlcpp_conversion.h>
#include <lib/ddk/debug.h>
#include <lib/fit/defer.h>
#include <lib/sysmem-version/sysmem-version.h>
#include <lib/trace/event.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <csignal>
#include <list>
#include <numeric>
#include <sstream>
#include <stack>
#include <type_traits>
#include "src/camera/drivers/controller/gdc_node.h"
#include "src/camera/drivers/controller/ge2d_node.h"
#include "src/camera/drivers/controller/input_node.h"
#include "src/camera/drivers/controller/output_node.h"
#include "src/camera/drivers/controller/passthrough_node.h"
#include "src/camera/drivers/controller/util.h"
#include "src/camera/lib/formatting/formatting.h"
#include "src/lib/digest/digest.h"
namespace camera {
PipelineManager::PipelineManager(async_dispatcher_t* dispatcher,
fuchsia::sysmem2::AllocatorSyncPtr sysmem_allocator,
const ddk::IspProtocolClient& isp,
const ddk::GdcProtocolClient& gdc,
const ddk::Ge2dProtocolClient& ge2d,
LoadFirmwareCallback load_firmware)
: dispatcher_(dispatcher),
isp_(isp),
gdc_(gdc),
ge2d_(ge2d),
memory_allocator_(std::move(sysmem_allocator)),
load_firmware_(std::move(load_firmware)) {}
PipelineManager::~PipelineManager() {
ZX_ASSERT_MSG(state_ == State::Uninitialized,
"Caller destroying pipeline manager without awaiting shutdown completion.");
}
void PipelineManager::SetRoots(const std::vector<InternalConfigNode>& roots) {
TRACE_DURATION("camera", "PipelineManager::SetRoots");
ZX_ASSERT(state_ == State::Uninitialized);
state_ = State::Configured;
current_roots_ = roots;
}
// Helper to check whether a config node supports a given stream type.
static bool Supports(const InternalConfigNode& node, fuchsia::camera2::CameraStreamType type) {
return std::any_of(node.supported_streams.cbegin(), node.supported_streams.cend(),
[type](const auto& supported) { return type == supported.type; });
}
void PipelineManager::ConfigureStreamPipeline(
StreamCreationData info, fidl::InterfaceRequest<fuchsia::camera2::Stream> request) {
TRACE_DURATION("camera", "PipelineManager::ConfigureStreamPipeline");
ZX_ASSERT_MSG(state_ == State::Configured,
"caller tried to create a new pipeline while the current one is still shutting "
"down or roots are not set");
// Queue this request for later execution if the pipeline is currently changing.
if (pipeline_changing_) {
pending_requests_.configure_stream_pipeline.emplace(std::move(info), std::move(request));
return;
}
// The flag is set defensively for the duration of this method. Although it always completes prior
// to returning control to the loop, this helps ensure that future changes to othjer parts of the
// method body do not introduce callbacks that would lead to erroneous behavior.
SetPipelineChanging(true);
// Start with an empty path and the roots of the current configuration.
FrameGraph::Node::Path path;
auto candidates = std::cref(info.roots);
// Advance down the configuration tree until an output node is found, creating intermediate nodes
// along the way.
while (!candidates.get().empty()) {
std::vector<uint8_t> matches;
ZX_ASSERT(candidates.get().size() < std::numeric_limits<uint8_t>::max());
for (uint8_t i = 0; i < candidates.get().size(); ++i) {
if (Supports(candidates.get()[i], info.stream_type())) {
matches.push_back(i);
}
}
if (matches.empty() || matches.size() > 1) {
const auto reason = matches.empty() ? "no matches" : "multiple matches";
const auto config_str = formatting::ToString(info.stream_type());
auto path_str = Format(path);
fprintf(stderr, "invalid product config - %s for stream type %s at path %s", reason,
config_str.c_str(), path_str.c_str());
abort();
}
path.push_back(matches[0]);
auto& current = candidates.get()[path.back()];
if (!graph_.Contains(path)) {
// If there is no node associated with the current config node, create it.
auto path_complete = CreateFGNode(info, path, request);
if (path_complete) {
SetPipelineChanging(false);
return;
}
}
candidates = current.child_nodes;
}
auto path_str = Format(path);
fprintf(stderr, "invalid product config - no outputs below path %s", path_str.c_str());
abort();
}
void PipelineManager::SetStreamingEnabled(bool enabled) {
streaming_enabled_ = enabled;
UpdateInputNodeStreamingState();
}
void PipelineManager::Shutdown(fit::closure callback) {
TRACE_DURATION("camera", "PipelineManager::Shutdown", "this", this);
zxlogf(INFO, "PipelineManager::Shutdown() - start");
ZX_ASSERT_MSG(state_ != State::ShuttingDown, "Caller requested shutdown multiple times.");
shutdown_state_.flow_nonce = TRACE_NONCE();
TRACE_FLOW_BEGIN("camera", "PipelineManager::ShutdownFlow", shutdown_state_.flow_nonce);
// Set the shutdown flag and save the callback to be invoked upon completion of shutdown.
state_ = State::ShuttingDown;
shutdown_state_.callback = std::move(callback);
// Discard any pending stream requests received before the shutdown, since they would just observe
// peer-closed anyway. Requests received subsequent to the shutdown will still be handled after it
// completes.
pending_requests_.configure_stream_pipeline = {};
// If the pipeline is changing, defer starting the shutdown.
if (pipeline_changing_) {
return;
}
// Otherwise, perform it immediately.
SetPipelineChanging(true);
ShutdownImpl();
}
void PipelineManager::ShutdownImpl() {
TRACE_DURATION("camera", "PipelineManager::ShutdownImpl", "this", this);
// Locate all active outputs in the graph. These are copied to a separate vector before removing
// them as doing so invalidates the map iterator.
shutdown_state_.started = true;
std::vector<std::vector<uint8_t>> output_paths;
for (auto& [key, node] : graph_.nodes) {
if (node.children.empty()) {
output_paths.push_back(key);
}
}
if (output_paths.empty()) {
SetPipelineChanging(false);
return;
}
for (auto& path : output_paths) {
ShutdownAndRemoveNode(path);
}
}
// TODO(https://fxbug.dev/42051249): the notion of "shutdown" being a required step
void PipelineManager::SetPipelineChanging(bool changing) {
TRACE_DURATION("camera", "PipelineManager::SetPipelineChanging", "changing", changing);
ZX_ASSERT_MSG(changing != pipeline_changing_, "pipeline already %s",
changing ? "changing" : "stable");
if (changing) {
pipeline_changing_ = true;
return;
}
// When transitioning from changing to stable, schedule a task to handle all accumulated requests.
// A task is used (vs. invoking the handlers immediately) in order to avoid unconstrained
// recursion. The requests are moved out of the main queue first in order to avoid an endless loop
// if the request relies on any async steps in order to complete.
async::PostTask(dispatcher_, [this] {
pipeline_changing_ = false;
// First handle any pending disconnects.
auto disconnects = std::move(pending_requests_.disconnects);
pending_requests_.disconnects = {};
while (!disconnects.empty()) {
ClientDisconnect(disconnects.front());
disconnects.pop();
}
// If any of the previous disconnects triggered a pipeline change, skip further handling as they
// will be handled upon subsequent transition to stable.
if (pipeline_changing_) {
return;
}
// Handle shutdown if it has been called. There should be no pending requests in this state.
if (state_ == State::ShuttingDown) {
ZX_ASSERT(pending_requests_.configure_stream_pipeline.empty());
if (shutdown_state_.started) {
// If the pipeline was actively shutting down, a transition to stable indicates this process
// has completed.
TRACE_FLOW_END("camera", "PipelineManager::ShutdownFlow", shutdown_state_.flow_nonce);
auto callback = std::move(shutdown_state_.callback);
shutdown_state_.callback = nullptr;
shutdown_state_.started = false;
state_ = State::Uninitialized;
callback();
} else {
// Otherwise, begin the shutdown process.
pipeline_changing_ = true;
ShutdownImpl();
}
return;
}
// Handle any pending connection requests.
auto requests = std::move(pending_requests_.configure_stream_pipeline);
pending_requests_.configure_stream_pipeline = {};
while (!requests.empty()) {
std::apply(fit::bind_member(this, &PipelineManager::ConfigureStreamPipeline),
std::move(requests.front()));
requests.pop();
}
});
}
void PipelineManager::UpdateInputNodeStreamingState() {
for (uint32_t i = 0; i < current_roots_.size(); ++i) {
auto input = graph_.nodes.find({static_cast<uint8_t>(i)});
if (input != graph_.nodes.end()) {
auto node = static_cast<InputNode*>(input->second.process_node.get());
// The input node is robust to idempotent start/stop requests.
std::stringstream ss;
ss << this << ": camera pipeline streaming " << (streaming_enabled_ ? "enabled" : "disabled");
zxlogf(INFO, "%s", ss.str().c_str());
if (streaming_enabled_) {
node->StartStreaming();
} else {
node->StopStreaming();
}
}
}
}
void PipelineManager::ShutdownAndRemoveNode(const std::vector<uint8_t>& path) {
TRACE_DURATION("camera", "PipelineManager::ShutdownAndRemoveNode", "path", Format(path));
ZX_ASSERT(pipeline_changing_);
graph_.nodes.at(path).process_node->Shutdown([this, path = path] {
auto target = graph_.nodes.extract(path);
auto parent = path;
parent.pop_back();
if (!parent.empty()) {
auto path_str = Format(path);
auto parent_str = Format(parent);
zxlogf(INFO, "Removing %s from %s child set", path_str.c_str(), parent_str.c_str());
graph_.nodes.at(parent).children.erase(path);
}
Prune();
});
}
void PipelineManager::Prune() {
TRACE_DURATION("camera", "PipelineManager::Prune");
ZX_ASSERT(pipeline_changing_);
for (auto& [key, value] : graph_.nodes) {
if (value.children.empty() && value.process_node->Type() != kOutputStream) {
async::PostTask(dispatcher_, [this, path = key] { ShutdownAndRemoveNode(path); });
return;
}
}
zxlogf(INFO, "Prune() - no prune targets found");
SetPipelineChanging(false);
}
std::vector<fuchsia::sysmem2::BufferCollectionConstraints> PipelineManager::GatherOutputConstraints(
const camera::InternalConfigNode& node) {
std::vector<fuchsia::sysmem2::BufferCollectionConstraints> constraints;
if (node.output_constraints) {
constraints.push_back(fidl::Clone(*node.output_constraints));
}
for (const auto& child : node.child_nodes) {
if (child.input_constraints) {
constraints.push_back(fidl::Clone(*child.input_constraints));
}
if (!child.output_constraints) {
auto child_output_constraints = GatherOutputConstraints(child);
for (auto& child_constraints : child_output_constraints) {
constraints.emplace_back(std::move(child_constraints));
}
}
}
return constraints;
}
std::string PipelineManager::Dump(bool log, cpp20::source_location location) const {
std::stringstream ss;
ss << "graph_:\n";
for (auto& [key, node] : graph_.nodes) {
ss << " " << Format(key) << ":\n";
ss << " path: " << Format(key) << "\n";
ss << " buffers: ";
if (node.output_buffers) {
auto& buffers = node.output_buffers->buffers;
ss << "[" << buffers.buffers().size() << "] "
<< (buffers.settings().buffer_settings().size_bytes() / 1024) << " KiB ("
<< buffers.settings().image_format_constraints().min_size().width << "x"
<< buffers.settings().image_format_constraints().min_size().height << ")\n";
} else {
ss << "<none>\n";
}
ss << " children: \n";
for (auto& child : node.children) {
ss << " " << Format(child) << "\n";
}
}
auto str = ss.str();
if (log) {
zxlogf(INFO, "%s: %s", location.function_name(), str.c_str());
}
return str;
}
// Returns a list of nodes corresponding to the given path.
// TODO(100525): Ideally the controller would just create a bidirectionally traversible
// representation of the product config when first loaded.
std::list<std::reference_wrapper<const InternalConfigNode>> GetPathICNodes(
const std::vector<InternalConfigNode>& roots, std::vector<uint8_t> path) {
if (path.empty()) {
return {};
}
std::list<std::reference_wrapper<const InternalConfigNode>> nodes{roots[path[0]]};
for (size_t i = 1; i < path.size(); ++i) {
nodes.push_back(nodes.back().get().child_nodes[path[i]]);
}
return nodes;
}
bool PipelineManager::CreateFGNode(const StreamCreationData& info, const std::vector<uint8_t>& path,
fidl::InterfaceRequest<fuchsia::camera2::Stream>& request) {
#ifndef NDEBUG
Dump();
#endif
ZX_ASSERT(!graph_.Contains(path));
auto path_icnodes = GetPathICNodes(info.roots, path);
auto& icself = path_icnodes.back().get();
FrameGraph::Node fgself{.pulldown{.interval = FramerateToInterval(icself.output_frame_rate)}};
ProcessNode::BufferAttachments attachments{};
if (icself.input_constraints) {
// Most nodes have input constraints (meaning it receives data from a preceding node). Search
// backwards along the current path for the closest producer node. This may not be the immediate
// parent, e.g. if the parent has no direct outputs and just operates "in place" on an input
// collection.
auto ancestor_path = path;
auto ancestor_icnodes = path_icnodes;
do {
ancestor_path.pop_back();
ancestor_icnodes.pop_back();
} while (!ancestor_path.empty() && !graph_.nodes.at(ancestor_path).output_buffers);
ZX_ASSERT(!ancestor_path.empty());
auto& ancestor_fgnode = graph_.nodes.at(ancestor_path);
attachments.input_collection = *ancestor_fgnode.output_buffers;
attachments.input_formats = ancestor_icnodes.back().get().image_formats;
}
if (icself.output_constraints) {
// Allocate a buffer collection by aggregating the current node's output constraints and all
// downstream input constraints. Downstream constraints propagate through "in place" processing
// nodes.
auto constraints = GatherOutputConstraints(icself);
BufferCollection buffers{};
zx_status_t status =
memory_allocator_.AllocateSharedMemory(constraints, buffers, NodeTypeName(icself));
if (status != ZX_OK) {
fprintf(stderr, "unable to allocate memory");
abort();
}
fgself.output_buffers = std::move(buffers);
attachments.output_collection = *fgself.output_buffers;
attachments.output_formats = icself.image_formats;
} else {
// A node that has no output constraints operates "in place" on the input collection it is
// provided, either modifying buffer content (GE2D) or sending it externally (output node). To
// avoid requiring special handling for these cases, the outputs are set to point to the same
// attachments as the inputs.
attachments.output_collection = attachments.input_collection;
attachments.output_formats = attachments.input_formats;
}
// The inter-node frame handler distributes an inbound frame to all active children.
auto frame_handler = [this, path](const ProcessNode::FrameToken& token,
frame_metadata_t metadata) {
std::stringstream ss;
auto& self_fnode = graph_.nodes.at(path);
for (auto& child_path : self_fnode.children) {
auto& child_fnode = graph_.nodes.at(child_path);
if (child_fnode.pulldown.accumulator <= numerics::Rational{0}) {
child_fnode.pulldown.accumulator += child_fnode.pulldown.interval;
ZX_ASSERT(child_fnode.process_node);
child_fnode.process_node->ProcessFrame(token, metadata);
}
child_fnode.pulldown.accumulator -= self_fnode.pulldown.interval;
}
};
switch (icself.type) {
// Input
case NodeType::kInputStream: {
auto result =
InputNode::Create(dispatcher_, attachments, std::move(frame_handler), isp_, info);
ZX_ASSERT(result.is_ok());
fgself.process_node = result.take_value();
} break;
// GDC
case NodeType::kGdc: {
auto result = GdcNode::Create(dispatcher_, attachments, std::move(frame_handler),
load_firmware_, gdc_, icself, info);
ZX_ASSERT(result.is_ok());
fgself.process_node = result.take_value();
} break;
// GE2D
case NodeType::kGe2d: {
auto result = camera::Ge2dNode::Create(dispatcher_, attachments, std::move(frame_handler),
load_firmware_, ge2d_, icself, info);
ZX_ASSERT(result.is_ok());
fgself.process_node = result.take_value();
} break;
// Output
case NodeType::kOutputStream: {
auto result = camera::OutputNode::Create(
dispatcher_, attachments, std::move(request),
{.disconnect = [this, path] { ClientDisconnect(path); },
.set_region_of_interest =
[this, path](auto x_min, auto y_min, auto x_max, auto y_max, auto callback) {
SetRegionOfInterest(path, x_min, y_min, x_max, y_max, std::move(callback));
},
.set_image_format =
[this, path](auto image_format_index, auto callback) {
SetImageFormat(path, image_format_index, std::move(callback));
},
.get_image_formats =
[this, path](auto callback) { GetImageFormats(path, std::move(callback)); },
.get_buffers = [this, path](auto callback) { GetBuffers(path, std::move(callback)); }});
ZX_ASSERT(result.is_ok());
fgself.process_node = result.take_value();
} break;
// Passthrough
case NodeType::kPassthrough: {
auto result = camera::PassthroughNode::Create(dispatcher_, attachments,
std::move(frame_handler), icself, info);
ZX_ASSERT(result.is_ok());
fgself.process_node = result.take_value();
} break;
default:
ZX_PANIC("unsupported stream type %d", icself.type);
}
fgself.process_node->SetLabel(Format(path) + "(" + NodeTypeName(icself) + ")");
// Once the node has been created successfully, attach it to its relatives and add it to the
// graph.
if (path.size() > 1) {
auto parent_path = FrameGraph::Node::Path{path.begin(), path.end() - 1};
auto& fgparent = graph_.nodes.at(parent_path);
fgparent.children.insert(path);
}
graph_.nodes.emplace(path, std::move(fgself));
#ifndef NDEBUG
Dump();
#endif
// Return true if the configured node was an output. This indicates that the `request` channel was
// consumed and the pipeline is complete.
return icself.type == kOutputStream;
}
void PipelineManager::ClientDisconnect(const std::vector<uint8_t>& origin) {
TRACE_DURATION("camera", "PipelineManager::ClientDisconnect", "origin", &origin);
auto origin_str = Format(origin);
zxlogf(INFO, "ClientDisconnect(%s)", origin_str.c_str());
if (pipeline_changing_) {
pending_requests_.disconnects.push(origin);
return;
}
SetPipelineChanging(true);
ShutdownAndRemoveNode(origin);
}
// NOLINTBEGIN(bugprone-easily-swappable-parameters): protocol defined elsewhere
void PipelineManager::SetRegionOfInterest(
const std::vector<uint8_t>& origin, float x_min, float y_min, float x_max, float y_max,
fuchsia::camera2::Stream::SetRegionOfInterestCallback callback) {
auto local_path = origin;
auto path_icnodes = GetPathICNodes(current_roots_, local_path);
while (!local_path.empty()) {
auto& icnode = path_icnodes.back().get();
auto& fgnode = graph_.nodes.at(local_path);
if (icnode.type == kGe2d && icnode.ge2d_info.config_type == GE2D_RESIZE) {
BindPolyMethod(fgnode.process_node.get(), &Ge2dNode::SetCropRect);
static_cast<Ge2dNode*>(fgnode.process_node.get())->SetCropRect(x_min, y_min, x_max, y_max);
}
local_path.pop_back();
path_icnodes.pop_back();
}
callback(ZX_OK);
}
// NOLINTEND(bugprone-easily-swappable-parameters)
void PipelineManager::SetImageFormat(const std::vector<uint8_t>& origin,
uint32_t image_format_index,
fuchsia::camera2::Stream::SetImageFormatCallback callback) {
// The existing implementation assumes that all nodes in the pipeline have either one image
// format or support the same number of formats. When a new format is requested, all nodes in
// the graph are notified. For most, the request does nothing.
auto local_path = origin;
auto path_icnodes = GetPathICNodes(current_roots_, local_path);
if (image_format_index >= path_icnodes.back().get().image_formats.size()) {
callback(ZX_ERR_INVALID_ARGS);
return;
}
while (!local_path.empty()) {
auto& icnode = path_icnodes.back().get();
auto& fgnode = graph_.nodes.at(local_path);
if (icnode.image_formats.size() > 1) {
fgnode.process_node->SetOutputFormat(image_format_index, [] {});
}
local_path.pop_back();
path_icnodes.pop_back();
}
callback(ZX_OK);
}
void PipelineManager::GetImageFormats(const std::vector<uint8_t>& origin,
fuchsia::camera2::Stream::GetImageFormatsCallback callback) {
auto path_icnodes = GetPathICNodes(current_roots_, origin);
auto& icnode = path_icnodes.back().get();
std::vector<::fuchsia::sysmem::ImageFormat_2> v1_image_formats;
for (auto& v2_image_format : icnode.image_formats) {
auto v2_image_format_natural = fidl::HLCPPToNatural(fidl::Clone(v2_image_format));
auto v1_image_format_result = sysmem::V1CopyFromV2ImageFormat(v2_image_format_natural);
ZX_ASSERT(v1_image_format_result.is_ok());
auto v1_image_format_hlcpp = fidl::NaturalToHLCPP(std::move(v1_image_format_result.value()));
v1_image_formats.emplace_back(std::move(v1_image_format_hlcpp));
}
callback(std::move(v1_image_formats));
}
void PipelineManager::GetBuffers(const std::vector<uint8_t>& origin,
fuchsia::camera2::Stream::GetBuffersCallback callback) {
// in_place nodes may not have bound buffer collection ptr, walk up to find the real collection.
auto local_path = origin;
local_path.pop_back();
while (!local_path.empty() && !graph_.nodes.at(local_path).output_buffers) {
local_path.pop_back();
}
if (local_path.empty()) {
auto origin_str = Format(origin);
fprintf(stderr, "no output buffers found in ancestors of %s", origin_str.c_str());
abort();
}
auto& fgnode = graph_.nodes.at(local_path);
auto& input_buffer_collection = fgnode.output_buffers->ptr;
ZX_ASSERT(input_buffer_collection);
fuchsia::sysmem2::BufferCollectionTokenHandle token;
fuchsia::sysmem2::BufferCollectionAttachTokenRequest attach_token_request;
attach_token_request.set_rights_attenuation_mask(ZX_RIGHT_SAME_RIGHTS);
attach_token_request.set_token_request(token.NewRequest());
input_buffer_collection->AttachToken(std::move(attach_token_request));
input_buffer_collection->Sync([callback = std::move(callback), token = std::move(token)](
fuchsia::sysmem2::Node_Sync_Result sync_result) mutable {
callback(fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken>(token.TakeChannel()));
});
}
bool PipelineManager::FrameGraph::Contains(const Node::Path& path) const {
return nodes.find(path) != nodes.end();
}
} // namespace camera