blob: 8541c00a50b8f2aa8c77f86832dc318dd8707314 [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/hw_accel/gdc/gdc.h"
#include <lib/ddk/debug.h>
#include <lib/ddk/driver.h>
#include <lib/ddk/hw/reg.h>
#include <lib/ddk/trace/event.h>
#include <lib/image-format/image_format.h>
#include <lib/syslog/cpp/macros.h>
#include <stdint.h>
#include <zircon/assert.h>
#include <zircon/threads.h>
#include <zircon/types.h>
#include <memory>
#include <fbl/algorithm.h>
#include <fbl/auto_lock.h>
#include "src/camera/drivers/hw_accel/gdc/bind.h"
#include "src/camera/drivers/hw_accel/gdc/gdc_regs.h"
namespace gdc {
namespace {
constexpr auto kTag = "gdc";
constexpr uint32_t kHiu = 0;
constexpr uint32_t kGdc = 1;
constexpr uint32_t kAxiAlignment = 16;
constexpr uint32_t kWordSize = 4;
// Expected maximum size (in bytes) GDC configuration.
constexpr uint32_t kGdcConfigurationSize = 16384;
// Expected maximum number of GDC configurations.
constexpr uint32_t kGdcConfigurationBufferCount = 4;
} // namespace
static inline uint32_t AxiWordAlign(zx_paddr_t value) {
ZX_DEBUG_ASSERT(value < std::numeric_limits<uint32_t>::max());
return fbl::round_up(static_cast<uint32_t>(value), kAxiAlignment);
}
void GdcDevice::InitClocks() {
// First reset the clocks.
GdcClkCntl::Get().ReadFrom(&clock_mmio_).reset_axi().reset_core().WriteTo(&clock_mmio_);
// Set the clocks to 8Mhz
// Source XTAL
// Clock divisor = 3
GdcClkCntl::Get()
.ReadFrom(&clock_mmio_)
.set_axi_clk_div(3)
.set_axi_clk_en(1)
.set_axi_clk_sel(0)
.set_core_clk_div(3)
.set_core_clk_en(1)
.set_core_clk_sel(0)
.WriteTo(&clock_mmio_);
// Enable GDC Power domain.
GdcMemPowerDomain::Get().ReadFrom(&clock_mmio_).set_gdc_pd(0).WriteTo(&clock_mmio_);
}
zx_status_t GdcDevice::GdcInitTask(const buffer_collection_info_2_t* input_buffer_collection,
const buffer_collection_info_2_t* output_buffer_collection,
const image_format_2_t* input_image_format,
const image_format_2_t* output_image_format_table_list,
size_t output_image_format_table_count,
uint32_t output_image_format_index,
const gdc_config_info* config_vmo_list, size_t config_vmos_count,
const hw_accel_frame_callback_t* frame_callback,
const hw_accel_res_change_callback* res_callback,
const hw_accel_remove_task_callback_t* remove_task_callback,
uint32_t* out_task_index) {
fbl::AutoLock al(&interface_lock_);
if (out_task_index == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
auto task = std::make_unique<GdcTask>();
zx_status_t status =
task->Init(input_buffer_collection, output_buffer_collection, input_image_format,
output_image_format_table_list, output_image_format_table_count,
output_image_format_index, config_vmo_list, config_vmos_count,
gdc_config_contig_vmos_, frame_callback, res_callback, remove_task_callback, bti_);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "Task Creation Failed";
return status;
}
// Put an entry in the hashmap.
task_map_[next_task_index_] = std::move(task);
*out_task_index = next_task_index_;
next_task_index_++;
return ZX_OK;
}
void GdcDevice::Start() const {
// Transition from 0->1 means GDC latches the data on the
// configuration ports and starts the processing.
// clang-format off
Config::Get()
.ReadFrom(gdc_mmio())
.set_start(0)
.WriteTo(gdc_mmio());
Config::Get()
.ReadFrom(gdc_mmio())
.set_start(1)
.WriteTo(gdc_mmio());
// clang-format on
}
void GdcDevice::Stop() const {
// clang-format off
Config::Get()
.ReadFrom(gdc_mmio())
.set_start(0)
.WriteTo(gdc_mmio());
// clang-format on
}
void GdcDevice::ProcessFrame(TaskInfo& info) {
TRACE_DURATION("camera", "GdcDevice::ProcessFrame", "task_index", info.task_index,
"input_buffer_index", info.index);
TRACE_FLOW_END("camera", "process_frame", info.index);
auto task = info.task;
// The way we have our SW instrumented, GDC should never be busy
// proccessing at this point. Doing a sanity check here to ensure
// that its not busy processing an image.
ZX_ASSERT(!Status::Get().ReadFrom(gdc_mmio()).busy());
auto input_buffer_index = info.index;
Stop();
// Program the GDC configuration registers.
auto size = task->GetConfigVmoSize(task->output_format_index()) / kWordSize;
auto addr = AxiWordAlign(task->GetConfigVmoPhysAddr(task->output_format_index()));
// clang-format off
ConfigAddr::Get()
.ReadFrom(gdc_mmio())
.set_config_addr(addr)
.WriteTo(gdc_mmio());
ZX_DEBUG_ASSERT(size < std::numeric_limits<uint32_t>::max());
ConfigSize::Get()
.ReadFrom(gdc_mmio())
.set_config_size(static_cast<uint32_t>(size))
.WriteTo(gdc_mmio());
// Program the Input frame details.
auto input_format = task->input_format();
DataInWidth::Get()
.ReadFrom(gdc_mmio())
.set_width(input_format.display_width)
.WriteTo(gdc_mmio());
DataInHeight::Get()
.ReadFrom(gdc_mmio())
.set_height(input_format.display_height)
.WriteTo(gdc_mmio());
// Program the Output frame details.
auto output_format = task->output_format();
DataOutWidth::Get()
.ReadFrom(gdc_mmio())
.set_width(output_format.display_width)
.WriteTo(gdc_mmio());
DataOutHeight::Get()
.ReadFrom(gdc_mmio())
.set_height(output_format.display_height)
.WriteTo(gdc_mmio());
// Program Data1In Address Register (Y).
zx_paddr_t input_y_addr;
auto input_line_offset = input_format.bytes_per_row;
ZX_ASSERT(ZX_OK == task->GetInputBufferPhysAddr(input_buffer_index, &input_y_addr));
Data1InAddr::Get()
.ReadFrom(gdc_mmio())
.set_addr(AxiWordAlign(input_y_addr))
.WriteTo(gdc_mmio());
// Program Data1In Offset Register (Y)
Data1InOffset::Get()
.ReadFrom(gdc_mmio())
.set_offset(input_line_offset)
.WriteTo(gdc_mmio());
// Program Data2In Address Register (UV).
auto input_uv_addr = input_y_addr + (input_line_offset * input_format.display_height);
Data2InAddr::Get()
.ReadFrom(gdc_mmio())
.set_addr(AxiWordAlign(input_uv_addr))
.WriteTo(gdc_mmio());
// Program Data2In Offset Register (UV)
Data2InOffset::Get()
.ReadFrom(gdc_mmio())
.set_offset(input_line_offset)
.WriteTo(gdc_mmio());
// clang-format on
// Now programming the output DMA registers.
// First fetch an unused buffer from the VMO pool.
auto result = task->GetOutputBufferPhysAddr();
if (result.is_error()) {
frame_available_info frame_info;
frame_info.frame_status = FRAME_STATUS_ERROR_BUFFER_FULL;
frame_info.metadata.input_buffer_index = input_buffer_index;
frame_info.metadata.timestamp = static_cast<uint64_t>(zx_clock_get_monotonic());
frame_info.metadata.image_format_index = task->output_format_index();
frame_info.metadata.capture_timestamp = info.capture_timestamp;
task->FrameReadyCallback(&frame_info);
return;
}
auto output_y_addr = result.value();
// Program Data1Out Address Register (Y).
auto output_line_offset = output_format.bytes_per_row;
// clang-format off
Data1OutAddr::Get()
.ReadFrom(gdc_mmio())
.set_addr(AxiWordAlign(output_y_addr))
.WriteTo(gdc_mmio());
// Program Data1Out Offset Register (Y)
Data1OutOffset::Get()
.ReadFrom(gdc_mmio())
.set_offset(output_line_offset)
.WriteTo(gdc_mmio());
// Program Data2Out Address Register (UV).
auto output_uv_addr = output_y_addr + (output_line_offset * output_format.display_height);
Data2OutAddr::Get()
.ReadFrom(gdc_mmio())
.set_addr(AxiWordAlign(output_uv_addr))
.WriteTo(gdc_mmio());
// Program Data2Out Offset Register (UV)
Data2OutOffset::Get()
.ReadFrom(gdc_mmio())
.set_offset(output_line_offset)
.WriteTo(gdc_mmio());
// clang-format on
zx_port_packet_t packet;
{
TRACE_DURATION("camera", "GdcDevice::WaitingForProcessingOnHwToFinish");
// Start GDC processing.
Start();
ZX_ASSERT(ZX_OK == WaitForInterrupt(&packet));
}
// Only Assert on ACK failure if its an actual HW interrupt.
// Currently we are injecting packets on the same ports for tests to
// fake an actual HW interrupt to test the callback functionality.
// This causes the IRQ object to be in a bad state when ACK'd.
if (packet.key == kPortKeyIrqMsg) {
ZX_ASSERT(gdc_irq_.ack() == ZX_OK);
}
if (packet.key == kPortKeyDebugFakeInterrupt || packet.key == kPortKeyIrqMsg) {
// Invoke the callback function and tell about the output buffer index
// which is ready to be used.
frame_available_info frame_info;
frame_info.frame_status = FRAME_STATUS_OK;
frame_info.buffer_id = task->GetOutputBufferIndex();
frame_info.metadata.input_buffer_index = input_buffer_index;
frame_info.metadata.timestamp = static_cast<uint64_t>(zx_clock_get_monotonic());
frame_info.metadata.image_format_index = task->output_format_index();
frame_info.metadata.capture_timestamp = info.capture_timestamp;
task->FrameReadyCallback(&frame_info);
}
}
void GdcDevice::ChangeOutputResolution(TaskInfo& info) {
TRACE_DURATION("camera", "GdcDevice::ChangeOutputResolution");
auto task = info.task;
task->set_output_format_index(info.index);
// Invoke the callback function and tell about the output buffer index
// which is ready to be used.
frame_available_info f_info;
f_info.frame_status = FRAME_STATUS_OK;
f_info.metadata.timestamp = static_cast<uint64_t>(zx_clock_get_monotonic());
f_info.metadata.image_format_index = task->output_format_index();
task->ResolutionChangeCallback(&f_info);
}
void GdcDevice::RemoveTask(TaskInfo& info) {
TRACE_DURATION("camera", "GdcDevice::RemoveTask");
fbl::AutoLock al(&interface_lock_);
auto task = info.task;
auto task_index = info.task_index;
// Find the entry in hashmap.
auto task_entry = task_map_.find(task_index);
if (task_entry == task_map_.end()) {
task->RemoveTaskCallback(TASK_REMOVE_STATUS_ERROR_INVALID);
return;
}
// Return the GDC VMOs from the task back to GcdDevice so that they can be reused by the
// next task to be created.
task->OnRemoveTask(gdc_config_contig_vmos_);
task->RemoveTaskCallback(TASK_REMOVE_STATUS_OK);
// Remove map entry.
task_map_.erase(task_entry);
}
void GdcDevice::ProcessTask(TaskInfo& info) {
switch (info.op) {
case GDC_OP_FRAME: {
return ProcessFrame(info);
}
case GDC_OP_SETOUTPUTRES: {
return ChangeOutputResolution(info);
}
case GDC_OP_REMOVE_TASK: {
return RemoveTask(info);
}
default: {
ZX_ASSERT_MSG(false, "Unknown GDC Op\n");
return;
}
}
}
int GdcDevice::FrameProcessingThread() {
FX_LOGST(TRACE, kTag) << "start";
for (;;) {
fbl::AutoLock al(&processing_queue_lock_);
while (processing_queue_.empty() && !shutdown_) {
frame_processing_signal_.Wait(&processing_queue_lock_);
}
if (shutdown_) {
break;
}
auto info = processing_queue_.back();
processing_queue_.pop_back();
al.release();
ProcessTask(info);
}
return ZX_OK;
}
zx_status_t GdcDevice::GdcSetOutputResolution(uint32_t task_index,
uint32_t new_output_image_format_index) {
fbl::AutoLock al(&interface_lock_);
// Find the entry in hashmap.
auto task_entry = task_map_.find(task_index);
if (task_entry == task_map_.end()) {
return ZX_ERR_INVALID_ARGS;
}
// Validate new image format index|.
if (!task_entry->second->IsOutputFormatIndexValid(new_output_image_format_index)) {
return ZX_ERR_INVALID_ARGS;
}
TaskInfo info;
info.op = GDC_OP_SETOUTPUTRES;
info.task = task_entry->second.get();
info.index = new_output_image_format_index;
info.task_index = task_index;
// Put the task on queue.
fbl::AutoLock lock(&processing_queue_lock_);
processing_queue_.push_front(info);
frame_processing_signal_.Signal();
return ZX_OK;
}
zx_status_t GdcDevice::GdcProcessFrame(uint32_t task_index, uint32_t input_buffer_index,
uint64_t capture_timestamp) {
TRACE_DURATION("camera", "GdcDevice::GdcProcessFrame");
TRACE_FLOW_BEGIN("camera", "process_frame", input_buffer_index);
fbl::AutoLock al(&interface_lock_);
// Find the entry in hashmap.
auto task_entry = task_map_.find(task_index);
if (task_entry == task_map_.end()) {
return ZX_ERR_INVALID_ARGS;
}
// Validate |input_buffer_index|.
if (!task_entry->second->IsInputBufferIndexValid(input_buffer_index)) {
return ZX_ERR_INVALID_ARGS;
}
TaskInfo info;
info.op = GDC_OP_FRAME;
info.task = task_entry->second.get();
info.index = input_buffer_index;
info.capture_timestamp = capture_timestamp;
info.task_index = task_index;
// Put the task on queue.
fbl::AutoLock lock(&processing_queue_lock_);
processing_queue_.push_front(info);
frame_processing_signal_.Signal();
return ZX_OK;
}
zx_status_t GdcDevice::StartThread() {
return thrd_status_to_zx_status(thrd_create_with_name(
&processing_thread_,
[](void* arg) -> int { return reinterpret_cast<GdcDevice*>(arg)->FrameProcessingThread(); },
this, "gdc-processing-thread"));
}
zx_status_t GdcDevice::StopThread() {
// Signal the worker thread and wait for it to terminate.
{
fbl::AutoLock al(&processing_queue_lock_);
shutdown_ = true;
frame_processing_signal_.Signal();
}
JoinThread();
return ZX_OK;
}
zx_status_t GdcDevice::WaitForInterrupt(zx_port_packet_t* packet) {
return port_.wait(zx::time::infinite(), packet);
}
void GdcDevice::GdcRemoveTask(uint32_t task_index) {
TRACE_DURATION("camera", "GdcDevice::GdcRemoveTask");
fbl::AutoLock al(&interface_lock_);
// Find the entry in hashmap.
auto task_entry = task_map_.find(task_index);
if (task_entry == task_map_.end()) {
// Release lock so death test doesn't hang.
al.release();
ZX_ASSERT(false);
}
TaskInfo info;
info.op = GDC_OP_REMOVE_TASK;
info.task = task_entry->second.get();
info.task_index = task_index;
// Put the task on the queue.
fbl::AutoLock lock(&processing_queue_lock_);
processing_queue_.push_front(info);
frame_processing_signal_.Signal();
}
void GdcDevice::GdcReleaseFrame(uint32_t task_index, uint32_t buffer_index) {
TRACE_DURATION("camera", "GdcDevice::GdcReleaseFrame");
fbl::AutoLock al(&interface_lock_);
// Find the entry in hashmap.
auto task_entry = task_map_.find(task_index);
if (task_entry == task_map_.end()) {
// Release lock so death test doesn't hang.
al.release();
ZX_ASSERT(false);
}
auto task = task_entry->second.get();
ZX_ASSERT(ZX_OK == task->ReleaseOutputBuffer(buffer_index));
}
// static
zx_status_t GdcDevice::Setup(void* /*ctx*/, zx_device_t* parent, std::unique_ptr<GdcDevice>* out) {
auto pdev = ddk::PDev::FromFragment(parent);
if (!pdev.is_valid()) {
FX_LOGST(ERROR, kTag) << "ZX_PROTOCOL_PDEV not available";
return ZX_ERR_NO_RESOURCES;
}
std::optional<fdf::MmioBuffer> clk_mmio;
zx_status_t status = pdev.MapMmio(kHiu, &clk_mmio);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "pdev_.MapMmio failed";
return status;
}
std::optional<fdf::MmioBuffer> gdc_mmio;
status = pdev.MapMmio(kGdc, &gdc_mmio);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "pdev_.MapMmio failed";
return status;
}
zx::interrupt gdc_irq;
status = pdev.GetInterrupt(0, &gdc_irq);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "pdev_.GetInterrupt failed";
return status;
}
zx::port port;
status = zx::port::create(ZX_PORT_BIND_TO_INTERRUPT, &port);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "port create failed";
return status;
}
status = gdc_irq.bind(port, kPortKeyIrqMsg, 0 /*options*/);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "interrupt bind failed";
return status;
}
zx::bti bti;
status = pdev.GetBti(0, &bti);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "could not obtain bti";
return status;
}
std::stack<zx::vmo> gdc_config_contig_vmos;
for (uint32_t i = 0; i < kGdcConfigurationBufferCount; i++) {
zx::vmo vmo;
status = zx::vmo::create_contiguous(bti, kGdcConfigurationSize, 0, &vmo);
if (status != ZX_OK) {
FX_LOGST(ERROR, kTag) << "Unable to create contiguous memory for GDC configuration VMO";
return ZX_ERR_NO_MEMORY;
}
gdc_config_contig_vmos.push(std::move(vmo));
}
auto gdc_device = std::make_unique<GdcDevice>(
parent, std::move(*clk_mmio), std::move(*gdc_mmio), std::move(gdc_config_contig_vmos),
std::move(gdc_irq), std::move(bti), std::move(port));
gdc_device->InitClocks();
status = gdc_device->StartThread();
*out = std::move(gdc_device);
return status;
}
void GdcDevice::DdkUnbind(ddk::UnbindTxn txn) {
ShutDown();
txn.Reply();
}
void GdcDevice::DdkRelease() {
StopThread();
delete this;
}
void GdcDevice::ShutDown() {}
zx_status_t GdcBind(void* ctx, zx_device_t* device) {
std::unique_ptr<GdcDevice> gdc_device;
zx_status_t status = gdc::GdcDevice::Setup(ctx, device, &gdc_device);
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "Could not setup gdc device";
return status;
}
zx_device_prop_t props[] = {
{BIND_PLATFORM_PROTO, 0, ZX_PROTOCOL_GDC},
};
status = gdc_device->DdkAdd(ddk::DeviceAddArgs("gdc").set_props(props));
if (status != ZX_OK) {
FX_PLOGST(ERROR, kTag, status) << "Could not add gdc device";
return status;
}
FX_LOGST(INFO, kTag) << "gdc driver added";
// gdc device intentionally leaked as it is now held by DevMgr.
__UNUSED auto* dev = gdc_device.release();
return status;
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = GdcBind;
return ops;
}();
} // namespace gdc
// clang-format off
ZIRCON_DRIVER(gdc, gdc::driver_ops, "gdc", "0.1");