blob: 6fffeeb04ff0c977e7c64f2f57e84e3595dcaeb4 [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 "src/camera/bin/camera-gym/buffer_collage.h"
#include <fuchsia/images/cpp/fidl.h>
#include <fuchsia/sysmem/cpp/fidl.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/async/cpp/wait.h>
#include <lib/async/dispatcher.h>
#include <lib/fit/bridge.h>
#include <lib/fit/promise.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/commands.h>
#include <lib/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/zx/eventpair.h>
#include <zircon/errors.h>
#include <zircon/syscalls/object.h>
#include <zircon/types.h>
#include <cmath>
namespace camera {
// Returns an event such that when the event is signaled and the dispatcher executed, the provided
// eventpair is closed. This can be used to bridge event- and eventpair-based fence semantics. If
// this function returns an error, |eventpair| is closed immediately.
fit::result<zx::event, zx_status_t> MakeEventBridge(async_dispatcher_t* dispatcher,
zx::eventpair eventpair) {
zx::event caller_event;
zx::event waiter_event;
zx_status_t status = zx::event::create(0, &caller_event);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
status = caller_event.duplicate(ZX_RIGHT_SAME_RIGHTS, &waiter_event);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
// A shared_ptr is necessary in order to begin the wait after setting the wait handler.
auto wait = std::make_shared<async::Wait>(waiter_event.get(), ZX_EVENT_SIGNALED);
wait->set_handler(
[wait, waiter_event = std::move(waiter_event), eventpair = std::move(eventpair)](
async_dispatcher_t* /*unused*/, async::Wait* /*unused*/, zx_status_t /*unused*/,
const zx_packet_signal_t* /*unused*/) mutable {
// Close the waiter along with its captures.
wait = nullptr;
});
status = wait->Begin(dispatcher);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
return fit::ok(std::move(caller_event));
}
BufferCollage::BufferCollage()
: loop_(&kAsyncLoopConfigNoAttachToCurrentThread), view_provider_binding_(this) {
SetStopOnError(scenic_);
SetStopOnError(allocator_);
view_provider_binding_.set_error_handler([this](zx_status_t status) {
FX_PLOGS(DEBUG, status) << "ViewProvider client disconnected.";
view_provider_binding_.Unbind();
});
}
BufferCollage::~BufferCollage() {
zx_status_t status =
async::PostTask(loop_.dispatcher(), fit::bind_member(this, &BufferCollage::Stop));
ZX_ASSERT(status == ZX_OK);
loop_.JoinThreads();
}
fit::result<std::unique_ptr<BufferCollage>, zx_status_t> BufferCollage::Create(
fuchsia::ui::scenic::ScenicHandle scenic, fuchsia::sysmem::AllocatorHandle allocator,
fit::closure stop_callback) {
auto collage = std::unique_ptr<BufferCollage>(new BufferCollage);
collage->start_time_ = zx::clock::get_monotonic();
// Bind interface handles and save the stop callback.
zx_status_t status = collage->scenic_.Bind(std::move(scenic), collage->loop_.dispatcher());
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
status = collage->allocator_.Bind(std::move(allocator), collage->loop_.dispatcher());
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
collage->stop_callback_ = std::move(stop_callback);
// Create a scenic session and set its event handlers.
collage->session_ =
std::make_unique<scenic::Session>(collage->scenic_.get(), collage->loop_.dispatcher());
collage->session_->set_error_handler(
fit::bind_member(collage.get(), &BufferCollage::OnScenicError));
collage->session_->set_event_handler(
fit::bind_member(collage.get(), &BufferCollage::OnScenicEvent));
// Start a thread and begin processing messages.
status = collage->loop_.StartThread("BufferCollage Loop");
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
return fit::error(status);
}
return fit::ok(std::move(collage));
}
fidl::InterfaceRequestHandler<fuchsia::ui::app::ViewProvider> BufferCollage::GetHandler() {
return fit::bind_member(this, &BufferCollage::OnNewRequest);
}
fit::promise<uint32_t> BufferCollage::AddCollection(
fuchsia::sysmem::BufferCollectionTokenHandle token, fuchsia::sysmem::ImageFormat_2 image_format,
std::string description) {
ZX_ASSERT(image_format.coded_width > 0);
ZX_ASSERT(image_format.coded_height > 0);
fit::bridge<uint32_t> task_bridge;
fit::closure add_collection = [this, token = std::move(token), image_format, description,
result = std::move(task_bridge.completer)]() mutable {
auto collection_id = next_collection_id_++;
FX_LOGS(DEBUG) << "Adding collection with ID " << collection_id << ".";
ZX_ASSERT(collection_views_.find(collection_id) == collection_views_.end());
auto& view = collection_views_[collection_id];
std::ostringstream oss;
oss << " (" << collection_id << ")";
SetStopOnError(view.collection, "Collection" + oss.str());
SetStopOnError(view.image_pipe, "Image Pipe" + oss.str());
view.image_format = image_format;
view.visible = false;
// Bind and duplicate the token.
fuchsia::sysmem::BufferCollectionTokenPtr token_ptr;
SetStopOnError(token_ptr);
zx_status_t status = token_ptr.Bind(std::move(token), loop_.dispatcher());
if (status != ZX_OK) {
FX_PLOGS(ERROR, status);
Stop();
result.complete_error();
return;
}
fuchsia::sysmem::BufferCollectionTokenHandle scenic_token;
token_ptr->Duplicate(ZX_RIGHT_SAME_RIGHTS, scenic_token.NewRequest());
allocator_->BindSharedCollection(std::move(token_ptr),
view.collection.NewRequest(loop_.dispatcher()));
// Sync the collection and create an image pipe using the scenic token.
view.collection->Sync([this, collection_id, token = std::move(scenic_token),
result = std::move(result)]() mutable {
auto& view = collection_views_[collection_id];
view.image_pipe_id = session_->AllocResourceId();
auto command = scenic::NewCreateImagePipe2Cmd(view.image_pipe_id,
view.image_pipe.NewRequest(loop_.dispatcher()));
session_->Enqueue(std::move(command));
view.image_pipe->AddBufferCollection(1, std::move(token));
UpdateLayout();
// Set minimal constraints then wait for buffer allocation.
view.collection->SetConstraints(true, {.usage{.none = fuchsia::sysmem::noneUsage}});
view.collection->WaitForBuffersAllocated(
[this, collection_id, result = std::move(result)](
zx_status_t status, fuchsia::sysmem::BufferCollectionInfo_2 buffers) mutable {
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to allocate buffers.";
Stop();
result.complete_error();
return;
}
collection_views_[collection_id].buffers = std::move(buffers);
// Add the negotiated images to the image pipe.
auto& view = collection_views_[collection_id];
for (uint32_t i = 0; i < view.buffers.buffer_count; ++i) {
view.image_pipe->AddImage(i + 1, 1, i, view.image_format);
}
// Complete the promise.
FX_LOGS(DEBUG) << "Successfully added collection " << collection_id << ".";
result.complete_ok(collection_id);
});
});
};
async::PostTask(loop_.dispatcher(), std::move(add_collection));
return task_bridge.consumer.promise();
}
void BufferCollage::RemoveCollection(uint32_t id) {
async::PostTask(loop_.dispatcher(), [this, id]() {
auto it = collection_views_.find(id);
if (it == collection_views_.end()) {
FX_LOGS(ERROR) << "Invalid collection ID " << id << ".";
Stop();
return;
}
auto& collection_view = it->second;
auto image_pipe_id = collection_view.image_pipe_id;
view_->DetachChild(*collection_view.node);
session_->ReleaseResource(image_pipe_id); // De-allocate ImagePipe2 scenic side
collection_view.collection->Close();
collection_views_.erase(it);
UpdateLayout();
});
}
void BufferCollage::PostShowBuffer(uint32_t collection_id, uint32_t buffer_index,
zx::eventpair release_fence,
std::optional<fuchsia::math::Rect> subregion) {
async::PostTask(loop_.dispatcher(), [=, release_fence = std::move(release_fence)]() mutable {
ShowBuffer(collection_id, buffer_index, std::move(release_fence), subregion);
});
}
void BufferCollage::PostSetCollectionVisibility(uint32_t id, bool visible) {
async::PostTask(loop_.dispatcher(), [=] {
auto it = collection_views_.find(id);
if (it == collection_views_.end()) {
FX_LOGS(ERROR) << "Invalid collection ID " << id << ".";
Stop();
return;
}
auto& view = it->second;
if (view.visible != visible) {
view.visible = visible;
UpdateLayout();
}
});
}
void BufferCollage::OnNewRequest(fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) {
if (view_provider_binding_.is_bound()) {
request.Close(ZX_ERR_ALREADY_BOUND);
return;
}
view_provider_binding_.Bind(std::move(request), loop_.dispatcher());
}
void BufferCollage::Stop() {
if (view_provider_binding_.is_bound()) {
FX_LOGS(WARNING) << "Collage closing view channel due to server error.";
view_provider_binding_.Close(ZX_ERR_INTERNAL);
}
scenic_ = nullptr;
allocator_ = nullptr;
view_ = nullptr;
collection_views_.clear();
loop_.Quit();
if (stop_callback_) {
stop_callback_();
stop_callback_ = nullptr;
}
}
template <typename T>
void BufferCollage::SetStopOnError(fidl::InterfacePtr<T>& p, std::string name) {
p.set_error_handler([this, name, &p](zx_status_t status) {
FX_PLOGS(ERROR, status) << name << " disconnected unexpectedly.";
p = nullptr;
Stop();
});
}
void BufferCollage::ShowBuffer(uint32_t collection_id, uint32_t buffer_index,
zx::eventpair release_fence,
std::optional<fuchsia::math::Rect> subregion) {
if (subregion) {
FX_LOGS(ERROR) << "Subregion is not yet supported.";
Stop();
return;
}
auto it = collection_views_.find(collection_id);
if (it == collection_views_.end()) {
FX_LOGS(ERROR) << "Invalid collection ID " << collection_id << ".";
Stop();
return;
}
if (buffer_index >= it->second.buffers.buffer_count) {
FX_LOGS(ERROR) << "Invalid buffer index " << buffer_index << ".";
Stop();
return;
}
auto result = MakeEventBridge(loop_.dispatcher(), std::move(release_fence));
if (result.is_error()) {
FX_PLOGS(ERROR, result.error());
Stop();
return;
}
std::vector<zx::event> scenic_fences;
scenic_fences.push_back(result.take_value());
it->second.image_pipe->PresentImage(buffer_index + 1, zx::clock::get_monotonic().get(), {},
std::move(scenic_fences),
[](fuchsia::images::PresentationInfo info) {});
}
// Calculate the grid size needed to fit |n| elements by alternately adding rows and columns.
static std::tuple<uint32_t, uint32_t> GetGridSize(uint32_t n) {
uint32_t rows = 0;
uint32_t cols = 0;
while (rows * cols < n) {
if (rows == cols) {
++cols;
} else {
++rows;
}
}
return {rows, cols};
}
// Calculate the center of an element |index| in a grid with |n| elements.
static std::tuple<float, float> GetCenter(uint32_t index, uint32_t n) {
auto [rows, cols] = GetGridSize(n);
uint32_t row = index / cols;
uint32_t col = index % cols;
float y = (row + 0.5f) / rows;
float x = (col + 0.5f) / cols;
// Center-align the last row if it is not fully filled.
if (row == rows - 1) {
x += static_cast<float>(rows * cols - n) * 0.5f / cols;
}
return {x, y};
}
// Calculate the size of an element scaled uniformly to fit a given extent.
static std::tuple<float, float> ScaleToFit(float element_width, float element_height,
float box_width, float box_height) {
float x_scale = box_width / element_width;
float y_scale = box_height / element_height;
float scale = std::min(x_scale, y_scale);
return {element_width * scale, element_height * scale};
}
// Builds a mesh that is equivalent to a scenic::Rectangle with the given |width| and |height|, but
// also includes a second smaller rectangle superimposed in the corner that shows a zoomed subregion
// of the associated material's center.
static std::unique_ptr<scenic::Mesh> BuildMesh(scenic::Session* session, float width,
float height) {
auto mesh = std::make_unique<scenic::Mesh>(session);
auto format = scenic::NewMeshVertexFormat(fuchsia::ui::gfx::ValueType::kVector3,
fuchsia::ui::gfx::ValueType::kNone,
fuchsia::ui::gfx::ValueType::kVector2);
constexpr float kMagnificationMargin = 0.02f;
constexpr float kMagnificationSize = 0.4f;
constexpr float kMagnificationAmount = 12.0f;
const float x1 = -width / 2;
const float x2 = width / 2;
const float y1 = -height / 2;
const float y2 = height / 2;
const float x3 = x1 + width * kMagnificationMargin;
const float x4 = x3 + width * kMagnificationSize;
const float y3 = y1 + height * kMagnificationMargin;
const float y4 = y3 + height * kMagnificationSize;
const float t1 = 0.5f - 0.5f / kMagnificationAmount;
const float t2 = 0.5f + 0.5f / kMagnificationAmount;
// clang-format off
std::vector<float> vb {
x1, y1, 0, 0, 0,
x2, y1, 0, 1, 0,
x1, y2, 0, 0, 1,
x2, y2, 0, 1, 1,
x3, y3, 0, t1, t1,
x4, y3, 0, t2, t1,
x3, y4, 0, t1, t2,
x4, y4, 0, t2, t2,
};
std::vector<uint32_t> ib {
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
};
//clang-format on
zx::vmo vb_vmo;
size_t vb_size = vb.size() * sizeof(vb[0]);
ZX_ASSERT(zx::vmo::create(vb_size, 0, &vb_vmo) == ZX_OK);
ZX_ASSERT(vb_vmo.write(vb.data(), 0, vb_size) == ZX_OK);
scenic::Memory vb_mem(session, std::move(vb_vmo), vb_size,
fuchsia::images::MemoryType::HOST_MEMORY);
scenic::Buffer scenic_vb(vb_mem, 0, vb_size);
zx::vmo ib_vmo;
size_t ib_size = ib.size() * sizeof(ib[0]);
ZX_ASSERT(zx::vmo::create(ib_size, 0, &ib_vmo) == ZX_OK);
ZX_ASSERT(ib_vmo.write(ib.data(), 0, ib_size) == ZX_OK);
scenic::Memory ib_mem(session, std::move(ib_vmo), ib_size,
fuchsia::images::MemoryType::HOST_MEMORY);
scenic::Buffer scenic_ib(ib_mem, 0, ib_size);
std::array<float, 3> aabb_min{x1, y1, 0};
std::array<float, 3> aabb_max{x2, y2, 0};
mesh->BindBuffers(scenic_ib, fuchsia::ui::gfx::MeshIndexFormat::kUint32, 0, ib.size(), scenic_vb,
format, 0, vb.size(), aabb_min, aabb_max);
return mesh;
}
void BufferCollage::UpdateLayout() {
// TODO(49070): resolve constraints even if node is not visible
// There is no intrinsic need to present the views prior to extents being known.
if (!view_extents_) {
constexpr fuchsia::ui::gfx::BoundingBox kDefaultBoundingBox{
.min{.x = 0, .y = 0, .z = 0}, .max{.x = 640, .y = 480, .z = 1024}};
view_extents_ = kDefaultBoundingBox;
}
auto [rows, cols] = GetGridSize(collection_views_.size());
float view_width = view_extents_->max.x - view_extents_->min.x;
float view_height = view_extents_->max.y - view_extents_->min.y;
constexpr float kPadding = 4.0f;
float cell_width = view_width / cols - kPadding;
float cell_height = view_height / rows - kPadding;
for (auto& [id, view] : collection_views_) {
if (view.node) {
view_->DetachChild(*view.node);
}
}
uint32_t index = 0;
for (auto& [id, view] : collection_views_) {
view.material = std::make_unique<scenic::Material>(session_.get());
view.material->SetTexture(view.image_pipe_id);
if (!view.visible) {
view.material->SetColor(0, 0, 0, 0);
}
auto [element_width, element_height] = ScaleToFit(
view.image_format.coded_width, view.image_format.coded_height, cell_width, cell_height);
view.mesh = BuildMesh(session_.get(), element_width, element_height);
view.node = std::make_unique<scenic::ShapeNode>(session_.get());
view.node->SetShape(*view.mesh);
view.node->SetMaterial(*view.material);
auto [x, y] = GetCenter(index++, collection_views_.size());
view.node->SetTranslation(view_width * x, view_height * y, 0);
// TODO(msandy): Track hidden nodes.
if (view_) {
view_->AddChild(*view.node);
}
}
if (heartbeat_indicator_.node) {
heartbeat_indicator_.node->SetTranslation(view_width * 0.5f, view_height, -1.0f);
}
}
void BufferCollage::OnScenicError(zx_status_t status) {
FX_PLOGS(ERROR, status) << "Scenic session error.";
Stop();
}
void BufferCollage::OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) {
for (const auto& event : events) {
if (event.is_gfx() && event.gfx().is_view_properties_changed()) {
auto aabb = event.gfx().view_properties_changed().properties.bounding_box;
// TODO(49069): bounding box should never be empty
if (aabb.max.x == aabb.min.x || aabb.max.y == aabb.min.y || aabb.max.z == aabb.min.z) {
view_extents_.reset();
} else {
view_extents_ = aabb;
}
UpdateLayout();
}
}
}
void BufferCollage::CreateView(
zx::eventpair view_token,
fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> incoming_services,
fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> outgoing_services) {
async::PostTask(loop_.dispatcher(), [this, view_token = std::move(view_token)]() mutable {
if (view_) {
FX_LOGS(ERROR) << "Clients may only call this method once per view provider lifetime.";
view_provider_binding_.Close(ZX_ERR_BAD_STATE);
Stop();
return;
}
view_ = std::make_unique<scenic::View>(
session_.get(), scenic::ToViewToken(std::move(view_token)), "Camera Gym");
heartbeat_indicator_.material = std::make_unique<scenic::Material>(session_.get());
constexpr float kIndicatorRadius = 12.0f;
heartbeat_indicator_.shape = std::make_unique<scenic::Circle>(session_.get(), kIndicatorRadius);
heartbeat_indicator_.node = std::make_unique<scenic::ShapeNode>(session_.get());
heartbeat_indicator_.node->SetShape(*heartbeat_indicator_.shape);
heartbeat_indicator_.node->SetMaterial(*heartbeat_indicator_.material);
view_->AddChild(*heartbeat_indicator_.node);
session_->set_on_frame_presented_handler(
[this](fuchsia::scenic::scheduling::FramePresentedInfo info) {
constexpr std::array kHeartbeatColor = {0xFF, 0x00, 0xFF};
constexpr auto kHeartbeatPeriod = zx::sec(1);
zx::duration t = (zx::clock::get_monotonic() - start_time_) % kHeartbeatPeriod.get();
float phase = static_cast<float>(t.to_msecs()) / kHeartbeatPeriod.to_msecs();
float amplitude = pow(1 - abs(1 - 2 * phase), 5.0f);
heartbeat_indicator_.material->SetColor(kHeartbeatColor[0] * amplitude,
kHeartbeatColor[1] * amplitude,
kHeartbeatColor[2] * amplitude, 0xFF);
session_->Present2(0, 0,
[](fuchsia::scenic::scheduling::FuturePresentationTimes times) {});
});
session_->Present2(0, 0, [](fuchsia::scenic::scheduling::FuturePresentationTimes times) {});
UpdateLayout();
});
}
} // namespace camera