| // 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/lib/virtual_camera/virtual_camera_impl.h" |
| |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fit/function.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <zircon/errors.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/types.h> |
| |
| #include <cmath> |
| #include <optional> |
| #include <sstream> |
| |
| #include <safemath/safe_conversions.h> |
| |
| namespace camera { |
| |
| fit::result<std::unique_ptr<VirtualCamera>, zx_status_t> VirtualCamera::Create( |
| fidl::InterfaceHandle<fuchsia::sysmem::Allocator> allocator) { |
| auto result = VirtualCameraImpl::Create(std::move(allocator)); |
| if (result.is_error()) { |
| return fit::error(result.error()); |
| } |
| return fit::ok(result.take_value()); |
| } |
| |
| VirtualCameraImpl::VirtualCameraImpl() : loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {} |
| |
| VirtualCameraImpl::~VirtualCameraImpl() { |
| async::PostTask(loop_.dispatcher(), fit::bind_member(this, &VirtualCameraImpl::OnDestruction)); |
| loop_.JoinThreads(); |
| } |
| |
| static fuchsia::camera3::StreamProperties DefaultStreamProperties() { |
| return {.image_format{.pixel_format{.type = fuchsia::sysmem::PixelFormatType::NV12}, |
| .coded_width = 1280, |
| .coded_height = 720, |
| .bytes_per_row = 1280, |
| .color_space{.type = fuchsia::sysmem::ColorSpaceType::REC601_NTSC}}, |
| .frame_rate{.numerator = 30, .denominator = 1}, |
| .supports_crop_region = false}; |
| } |
| |
| static fuchsia::sysmem::BufferCollectionConstraints DefaultBufferConstraints() { |
| return {.usage = {.cpu = fuchsia::sysmem::cpuUsageWrite}, |
| .min_buffer_count_for_camping = 2, |
| .has_buffer_memory_constraints = true, |
| .buffer_memory_constraints{.ram_domain_supported = true}, |
| .image_format_constraints_count = 1, |
| .image_format_constraints = {{{ |
| .pixel_format = DefaultStreamProperties().image_format.pixel_format, |
| .color_spaces_count = 1, |
| .color_space = {DefaultStreamProperties().image_format.color_space}, |
| .min_coded_width = DefaultStreamProperties().image_format.coded_width, |
| .max_coded_width = DefaultStreamProperties().image_format.coded_width, |
| .min_coded_height = DefaultStreamProperties().image_format.coded_height, |
| .max_coded_height = DefaultStreamProperties().image_format.coded_height, |
| .coded_width_divisor = 128, |
| }}}}; |
| } |
| |
| constexpr uint32_t kEmbeddedMetadataMagic = 0x5643414D; // "VCAM" |
| |
| constexpr size_t kEmbeddedMetadataNumBits = |
| CHAR_BIT * (sizeof(kEmbeddedMetadataMagic) + sizeof(fuchsia::camera3::FrameInfo::buffer_index) + |
| sizeof(fuchsia::camera3::FrameInfo::frame_counter) + |
| sizeof(fuchsia::camera3::FrameInfo::timestamp)); |
| |
| // Embeds the bytes of |value| into the memory specified by |data| by overwriting the least |
| // significant bit of bytes at fixed offsets into the data stream. Returns the size of |data| |
| // consumed by embedding the metadata. |
| template <class T> |
| static size_t Embed(char* data, T value, size_t total_buffer_size) { |
| size_t offset = 0; |
| const size_t offset_per_bit = total_buffer_size / kEmbeddedMetadataNumBits; |
| for (size_t i = 0; i < sizeof(value) * CHAR_BIT; ++i) { |
| data[offset] &= ~1; |
| data[offset] |= (value >> i) & 1; |
| offset += offset_per_bit; |
| } |
| return offset; |
| } |
| |
| // Reverses the process of Embed, constructing a value from the least significant bits of various |
| // bytes in |data|. Returns the size of |data| consumed by extracting the metadata. |
| template <class T> |
| static size_t Extract(const char* data, T& value, size_t total_buffer_size) { |
| value = 0; |
| size_t offset = 0; |
| const size_t offset_per_bit = total_buffer_size / kEmbeddedMetadataNumBits; |
| for (size_t i = 0; i < sizeof(value) * CHAR_BIT; ++i) { |
| value |= static_cast<T>(data[offset] & 1) << i; |
| offset += offset_per_bit; |
| } |
| return offset; |
| } |
| |
| fit::result<std::unique_ptr<VirtualCamera>, zx_status_t> VirtualCameraImpl::Create( |
| fidl::InterfaceHandle<fuchsia::sysmem::Allocator> allocator) { |
| auto camera = std::make_unique<VirtualCameraImpl>(); |
| |
| ZX_ASSERT(camera->allocator_.Bind(std::move(allocator), camera->loop_.dispatcher()) == ZX_OK); |
| camera->allocator_.set_error_handler([camera = camera.get()](zx_status_t status) { |
| FX_PLOGS(ERROR, status) << "fuchsia.sysmem.Allocator server disconnected."; |
| camera->camera_ = nullptr; |
| }); |
| |
| auto stream_result = |
| FakeStream::Create(DefaultStreamProperties(), |
| fit::bind_member(camera.get(), &VirtualCameraImpl::OnSetBufferCollection)); |
| if (stream_result.is_error()) { |
| FX_PLOGS(ERROR, stream_result.error()) << "Failed to create fake stream."; |
| return fit::error(ZX_ERR_INTERNAL); |
| } |
| |
| camera->stream_ = stream_result.take_value(); |
| |
| camera::FakeConfiguration config; |
| config.push_back(camera->stream_); |
| std::vector<camera::FakeConfiguration> configs; |
| configs.push_back(std::move(config)); |
| auto camera_result = FakeCamera::Create("VirtualCamera", std::move(configs)); |
| if (camera_result.is_error()) { |
| FX_PLOGS(ERROR, camera_result.error()) << "Failed to create fake camera."; |
| return fit::error(ZX_ERR_INTERNAL); |
| } |
| |
| camera->camera_ = camera_result.take_value(); |
| |
| ZX_ASSERT(camera->loop_.StartThread("Virtual Camera Loop") == ZX_OK); |
| |
| camera->interrupt_start_time_ = zx::clock::get_monotonic(); |
| camera->frame_count_ = 0; |
| ZX_ASSERT(async::PostTask(camera->loop_.dispatcher(), |
| fit::bind_member(camera.get(), &VirtualCameraImpl::FrameTick)) == |
| ZX_OK); |
| |
| return fit::ok(std::move(camera)); |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::camera3::Device> VirtualCameraImpl::GetHandler() { |
| return camera_->GetHandler(); |
| } |
| |
| fit::result<void, std::string> VirtualCameraImpl::CheckFrame( |
| const void* data, size_t size, const fuchsia::camera3::FrameInfo& info) { |
| if (size != buffers_->settings.buffer_settings.size_bytes) { |
| return fit::error("Data size does not match buffer size."); |
| } |
| |
| const char* image_data = reinterpret_cast<const char*>(data); |
| |
| uint32_t embedded_magic = 0; |
| image_data += Extract(image_data, embedded_magic, size); |
| if (embedded_magic != kEmbeddedMetadataMagic) { |
| std::ostringstream oss; |
| oss << "Magic value mismatch: expected " << kEmbeddedMetadataMagic << ", got " << embedded_magic |
| << "."; |
| return fit::error(oss.str()); |
| } |
| |
| fuchsia::camera3::FrameInfo embedded_info{}; |
| |
| image_data += Extract(image_data, embedded_info.buffer_index, size); |
| if (embedded_info.buffer_index != info.buffer_index) { |
| std::ostringstream oss; |
| oss << "Buffer index mismatch: data = " << embedded_info.buffer_index |
| << ", info = " << info.buffer_index << "."; |
| return fit::error(oss.str()); |
| } |
| |
| image_data += Extract(image_data, embedded_info.frame_counter, size); |
| if (embedded_info.frame_counter != info.frame_counter) { |
| std::ostringstream oss; |
| oss << "Frame counter mismatch: data = " << embedded_info.frame_counter |
| << ", info = " << info.frame_counter << "."; |
| return fit::error(oss.str()); |
| } |
| |
| image_data += Extract(image_data, embedded_info.timestamp, size); |
| if (embedded_info.timestamp != info.timestamp) { |
| std::ostringstream oss; |
| oss << "Timestamp mismatch: data = " << embedded_info.timestamp << ", info = " << info.timestamp |
| << "."; |
| return fit::error(oss.str()); |
| } |
| |
| return fit::ok(); |
| } |
| |
| void VirtualCameraImpl::OnDestruction() { |
| loop_.Quit(); |
| for (auto& it : frame_waiters_) { |
| // TODO(fxbug.dev/50018): async::Wait destructor ordering edge case |
| it.second->Cancel(); |
| it.second = nullptr; |
| } |
| camera_ = nullptr; |
| } |
| |
| void VirtualCameraImpl::OnSetBufferCollection( |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) { |
| buffers_ = std::nullopt; |
| while (!free_buffers_.empty()) { |
| free_buffers_.pop(); |
| } |
| |
| fuchsia::sysmem::BufferCollectionPtr collection; |
| allocator_->BindSharedCollection(std::move(token), collection.NewRequest(loop_.dispatcher())); |
| collection.set_error_handler([this](zx_status_t status) { |
| FX_PLOGS(ERROR, status) << "BufferCollection server disconnected."; |
| camera_ = nullptr; |
| }); |
| collection->SetConstraints(true, DefaultBufferConstraints()); |
| collection->WaitForBuffersAllocated( |
| [this, collection = std::move(collection)](zx_status_t status, |
| fuchsia::sysmem::BufferCollectionInfo_2 buffers) { |
| collection->Close(); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to allocate buffers."; |
| camera_ = nullptr; |
| return; |
| } |
| buffers_ = std::move(buffers); |
| for (uint32_t i = 0; i < buffers_->buffer_count; ++i) { |
| free_buffers_.push(i); |
| } |
| }); |
| } |
| |
| void VirtualCameraImpl::FrameTick() { |
| ++frame_count_; |
| const uint64_t frame_time_ns = (1000000ull * DefaultStreamProperties().frame_rate.denominator) / |
| DefaultStreamProperties().frame_rate.numerator; |
| auto deadline = interrupt_start_time_ + zx::usec(frame_count_ * frame_time_ns); |
| ZX_ASSERT(async::PostTaskForTime(loop_.dispatcher(), |
| fit::bind_member(this, &VirtualCameraImpl::FrameTick), |
| deadline) == ZX_OK); |
| |
| if (!buffers_.has_value()) { |
| return; |
| } |
| |
| if (free_buffers_.size() == DefaultBufferConstraints().min_buffer_count_for_camping) { |
| return; |
| } |
| |
| auto buffer_index = free_buffers_.front(); |
| free_buffers_.pop(); |
| |
| FillFrame(buffer_index); |
| auto timestamp = zx::clock::get_monotonic(); |
| |
| zx::eventpair fence; |
| fuchsia::camera3::FrameInfo info; |
| info.frame_counter = frame_count_; |
| info.buffer_index = buffer_index; |
| info.timestamp = timestamp.get(); |
| ZX_ASSERT(zx::eventpair::create(0, &fence, &info.release_fence) == ZX_OK); |
| EmbedMetadata(info); |
| |
| stream_->AddFrame(std::move(info)); |
| |
| auto waiter = |
| std::make_unique<async::Wait>(fence.get(), ZX_EVENTPAIR_PEER_CLOSED, 0, |
| [this, fence = std::move(fence), buffer_index]( |
| async_dispatcher_t* dispatcher, async::WaitBase* wait, |
| zx_status_t status, const zx_packet_signal_t* signal) { |
| free_buffers_.push(buffer_index); |
| frame_waiters_.erase(buffer_index); |
| }); |
| ZX_ASSERT(waiter->Begin(loop_.dispatcher()) == ZX_OK); |
| frame_waiters_[buffer_index] = std::move(waiter); |
| } |
| |
| void VirtualCameraImpl::FillFrame(uint32_t buffer_index) { |
| auto& buffer = buffers_->buffers[buffer_index]; |
| |
| fzl::VmoMapper mapper; |
| const size_t map_size = buffers_->settings.buffer_settings.size_bytes - buffer.vmo_usable_start; |
| zx_status_t status = |
| mapper.Map(buffer.vmo, buffer.vmo_usable_start, map_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to map buffer."; |
| return; |
| } |
| |
| auto format = DefaultStreamProperties().image_format; |
| auto data = reinterpret_cast<char*>(mapper.start()); |
| |
| // Render the UV plane with time-varying Y. |
| |
| // Luma |
| const uint8_t y = safemath::checked_cast<uint8_t>( |
| 128.0f + |
| 127.5f * sinf(safemath::checked_cast<float>(2 * M_PI * (frame_count_ % 240) / 240.0))); |
| for (uint32_t row = 0; row < format.coded_height; ++row) { |
| memset(data, y, format.bytes_per_row); |
| data += format.bytes_per_row; |
| } |
| |
| // Chroma |
| const uint32_t chroma_width = format.coded_width / 2; |
| const uint32_t chroma_height = format.coded_height / 2; |
| for (uint32_t row = 0; row < chroma_height; ++row) { |
| for (uint32_t col = 0; col < chroma_width; ++col) { |
| const auto u = safemath::checked_cast<uint8_t>((col * 256) / chroma_width); |
| const auto v = safemath::checked_cast<uint8_t>(255 - (row * 256) / chroma_height); |
| data[col * 2] = u; |
| data[col * 2 + 1] = v; |
| } |
| data += format.bytes_per_row; |
| } |
| |
| zx_cache_flush(mapper.start(), mapper.size(), ZX_CACHE_FLUSH_DATA); |
| mapper.Unmap(); |
| } |
| |
| void VirtualCameraImpl::EmbedMetadata(const fuchsia::camera3::FrameInfo& info) { |
| auto& buffer = buffers_->buffers[info.buffer_index]; |
| |
| fzl::VmoMapper mapper; |
| auto buffer_size = buffers_->settings.buffer_settings.size_bytes; |
| const size_t map_size = buffer_size - buffer.vmo_usable_start; |
| zx_status_t status = |
| mapper.Map(buffer.vmo, buffer.vmo_usable_start, map_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to map buffer."; |
| return; |
| } |
| auto image_data = reinterpret_cast<char*>(mapper.start()); |
| |
| image_data += Embed(image_data, kEmbeddedMetadataMagic, buffer_size); |
| image_data += Embed(image_data, info.buffer_index, buffer_size); |
| image_data += Embed(image_data, info.frame_counter, buffer_size); |
| image_data += Embed(image_data, info.timestamp, buffer_size); |
| |
| mapper.Unmap(); |
| } |
| |
| } // namespace camera |