blob: 9044140c9f5a2e02caeb1d91062ec92f7737d332 [file] [log] [blame]
// Copyright 2022 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/usb_video/usb_state.h"
#include <lib/ddk/debug.h>
#include <lib/zircon-internal/align.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <memory>
#include <fbl/algorithm.h>
#include <fbl/auto_lock.h>
#include <usb/usb-request.h>
#include <usb/usb.h>
#include "src/lib/listnode/listnode.h"
namespace camera::usb_video {
static constexpr uint32_t MAX_OUTSTANDING_REQS = 8;
namespace {
// The biggest size we are allowing USB descriptor strings to be.
// Technically, the bLength field for strings is one byte, so the
// max size for any string should be 255.
static constexpr size_t kUsbDescriptorStringSize = 256;
// Extract strings from the device description.
// Strings in the usb device description are represented as indices
// into the 'strings' section at the end. This function unpacks a specific
// string, given its index.
std::string FetchString(const usb_protocol_t& usb_proto, uint8_t description_index) {
uint8_t str_buf[kUsbDescriptorStringSize];
size_t buflen = sizeof(str_buf);
uint16_t language_id = 0;
zx_status_t res = usb_get_string_descriptor(&usb_proto, description_index, language_id,
&language_id, str_buf, buflen, &buflen);
if (res != ZX_OK) {
return std::string();
}
buflen = std::min(buflen, sizeof(str_buf));
return std::string(reinterpret_cast<char*>(str_buf), buflen);
}
void print_controls(const usb_video_vc_probe_and_commit_controls& proposal) {
zxlogf(DEBUG, "bmHint 0x%x", proposal.bmHint);
zxlogf(DEBUG, "bFormatIndex: %u", proposal.bFormatIndex);
zxlogf(DEBUG, "bFrameIndex: %u", proposal.bFrameIndex);
zxlogf(DEBUG, "dwFrameInterval: %u", proposal.dwFrameInterval);
zxlogf(DEBUG, "dwMaxVideoFrameSize: %u", proposal.dwMaxVideoFrameSize);
zxlogf(DEBUG, "dwMaxPayloadTransferSize: %u", proposal.dwMaxPayloadTransferSize);
}
zx::result<usb_video_vc_probe_and_commit_controls> ClearIfIoErrors(zx_status_t status,
usb_protocol_t* usb) {
if (status == ZX_ERR_IO_REFUSED || status == ZX_ERR_IO_INVALID) {
usb_reset_endpoint(usb, 0);
}
zxlogf(ERROR, "usb_video_negotiate_probe failed: %d", status);
return zx::error(status);
}
// TODO(fxbug.dev/104233): Use of dwMaxVideoFrameBufferSize for certain formats has been
// deprecated. The dwMaxVideoFrameSize field obtained here should be used instead.
zx::result<usb_video_vc_probe_and_commit_controls> ProbeAndCommit(usb_protocol_t* usb,
uint8_t iface_num,
uint8_t format_index,
uint8_t frame_index,
uint32_t default_frame_interval) {
usb_video_vc_probe_and_commit_controls proposal;
memset(&proposal, 0, sizeof(usb_video_vc_probe_and_commit_controls));
proposal.bmHint = USB_VIDEO_BM_HINT_FRAME_INTERVAL;
proposal.bFormatIndex = format_index;
proposal.bFrameIndex = frame_index;
proposal.dwFrameInterval = default_frame_interval;
zx_status_t status;
zxlogf(DEBUG, "usb_video_negotiate_probe: PROBE_CONTROL SET_CUR");
print_controls(proposal);
// The wValue field (the fourth parameter) specifies the Control Selector
// (in this case USB_VIDEO_VS_PROBE_CONTROL) in the high byte,
// and the low byte must be set to zero.
// See UVC 1.5 Spec. 4.2.1 Interface Control Requests.
status = usb_control_out(usb, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_VIDEO_SET_CUR, USB_VIDEO_VS_PROBE_CONTROL << 8, iface_num,
ZX_TIME_INFINITE, (uint8_t*)&proposal, sizeof(proposal));
if (status != ZX_OK) {
return ClearIfIoErrors(status, usb);
}
// The length of returned result varies, so zero this out before hand.
usb_video_vc_probe_and_commit_controls result;
memset(&result, 0, sizeof(usb_video_vc_probe_and_commit_controls));
zxlogf(DEBUG, "usb_video_negotiate_probe: PROBE_CONTROL GET_CUR");
size_t out_length;
status = usb_control_in(usb, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE, USB_VIDEO_GET_CUR,
USB_VIDEO_VS_PROBE_CONTROL << 8, iface_num, ZX_TIME_INFINITE,
(uint8_t*)&result, sizeof(result), &out_length);
if (status != ZX_OK) {
return ClearIfIoErrors(status, usb);
}
// Fields after dwMaxPayloadTransferSize are optional, only 26 bytes are
// guaranteed.
if (out_length < 26) {
zxlogf(ERROR, "usb_video_negotiate_probe: got length %lu, want >= 26", out_length);
} else {
print_controls(result);
}
uint32_t dwMaxPayloadTransferSize = result.dwMaxPayloadTransferSize;
status = usb_control_out(usb, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_VIDEO_SET_CUR, USB_VIDEO_VS_COMMIT_CONTROL << 8, iface_num,
ZX_TIME_INFINITE, (uint8_t*)&result, sizeof(result));
if (status != ZX_OK) {
if (status == ZX_ERR_IO_REFUSED || status == ZX_ERR_IO_INVALID) {
// clear the stall/error
usb_reset_endpoint(usb, 0);
}
zxlogf(ERROR, "usb_video_negotiate_commit failed: %d", status);
return zx::error(status);
}
ZX_DEBUG_ASSERT(result.dwMaxPayloadTransferSize == dwMaxPayloadTransferSize);
return zx::ok(result);
}
} // namespace
// static
std::string UsbState::State2String(UsbState::StreamingState state) {
switch (state) {
case UsbState::StreamingState::STOPPED:
return "STOPPED";
case UsbState::StreamingState::READY:
return "READY";
case UsbState::StreamingState::STREAMING:
return "STREAMING";
case UsbState::StreamingState::STOPPING:
return "STOPPING";
}
return "INVALID STATE";
}
// static
zx::result<UsbRequest> UsbRequest::Create(usb_protocol_t* usb, uint8_t ep_addr, uint64_t size) {
uint64_t parent_req_size = usb_get_request_size(usb);
if (!parent_req_size) {
return zx::error(ZX_ERR_INTERNAL);
}
usb_request_t* req;
auto status = usb_request_alloc(&req, size, ep_addr, parent_req_size);
if (status != ZX_OK) {
zxlogf(ERROR, "usb_request_alloc failed: %d", status);
return zx::error(status);
}
return zx::ok(UsbRequest(req));
}
UsbRequest::~UsbRequest() {
if (req_) {
usb_request_release(req_);
}
}
const UsbDeviceInfo UsbState::GetDeviceInfo() const {
usb_device_descriptor_t usb_dev_desc;
UsbDeviceInfo device_info;
// Fetch our top level device descriptor, so we know stuff like the values
// of our VID/PID.
usb_get_device_descriptor(&usb_, &usb_dev_desc);
device_info.vendor_id = usb_dev_desc.id_vendor;
device_info.product_id = usb_dev_desc.id_product;
// Attempt to fetch the string descriptors for our manufacturer name,
// product name, and serial number.
if (usb_dev_desc.i_manufacturer) {
device_info.manufacturer = FetchString(usb_, usb_dev_desc.i_manufacturer);
}
if (usb_dev_desc.i_product) {
device_info.product_name = FetchString(usb_, usb_dev_desc.i_product);
}
if (usb_dev_desc.i_serial_number) {
device_info.serial_number = FetchString(usb_, usb_dev_desc.i_serial_number);
}
return device_info;
}
UsbState::UsbState(usb_protocol_t usb, StreamingSetting settings)
: usb_(usb),
usb_ep_addr_(settings.input_header.bEndpointAddress),
iface_num_(settings.vs_interface.b_interface_number),
dev_clock_frequency_hz_(settings.hw_clock_frequency),
streaming_settings_(std::move(settings)),
req_complete_callback_({
.callback =
[](void* ctx, usb_request_t* request) {
ZX_DEBUG_ASSERT(ctx != nullptr);
reinterpret_cast<UsbState*>(ctx)->RequestComplete(request);
},
.ctx = this,
}) {}
zx_status_t UsbState::StartStreaming(fit::function<void(usb_request_t*)> req_callback) {
fbl::AutoLock lock(&lock_);
// We should never receive commands while in the STOPPING state.
// We consider it an error to call StartStreaming while already streaming.
if (streaming_state_ != StreamingState::READY) {
if (streaming_state_ == StreamingState::STOPPED) {
zxlogf(ERROR, "SetFormat must be called before streaming can start.");
} else {
zxlogf(ERROR,
"StartStreaming is only allowed while UsbState is in READY state. Current state: %s",
State2String(streaming_state_).c_str());
}
return ZX_ERR_BAD_STATE;
}
zxlogf(DEBUG, "UsbVideoStream::usb_set_interface");
zx_status_t status = usb_set_interface(&usb_, iface_num_, alt_setting_);
if (status != ZX_OK) {
return status;
}
req_callback_ = std::move(req_callback);
ZX_DEBUG_ASSERT(free_requests_.size() == allocated_requests_.size());
while (free_requests_.size()) {
usb_request_queue(&usb_, free_requests_.back(), &req_complete_callback_);
free_requests_.pop_back();
}
streaming_state_ = StreamingState::STREAMING;
return ZX_OK;
}
zx_status_t UsbState::StopStreaming() {
{
fbl::AutoLock lock(&lock_);
if (streaming_state_ != StreamingState::STREAMING) {
if (streaming_state_ == StreamingState::STOPPING) {
zxlogf(ERROR, "Detected temporary STOPPING state while calling StopStreaming.");
} else {
// streaming_state_ == STOPPED or READY
zxlogf(ERROR, "Called StopStreaming, but stream was in state:%s.",
State2String(streaming_state_).c_str());
}
return ZX_ERR_BAD_STATE;
}
// We can now assume that our current state is STREAMING.
// Set the temporary state STOPPING. This should prevent any other changes to state.
streaming_state_ = StreamingState::STOPPING;
} // Release lock b/c usb_cancel_all calls all callbacks synchronously
// Calling cancel all here to flush out all the requests we have queued.
usb_cancel_all(&usb_, usb_ep_addr_);
fbl::AutoLock lock(&lock_);
ZX_ASSERT_MSG(streaming_state_ == StreamingState::STOPPING,
"Error: The streaming state changed unexpectedly from STOPPING during "
"StopStreaming()");
zx_status_t status = ZX_OK;
if (free_requests_.size() != allocated_requests_.size()) {
zxlogf(ERROR, "Canceling the stream did not drain all the remaining requests.");
// Not much to do at this point. Perhaps reset the usb in a more drastic manner?
// At the very least, return an error to indicate something went wrong.
status = ZX_ERR_INTERNAL;
} else {
zxlogf(DEBUG, "setting video buffer as stopped");
}
streaming_state_ = StreamingState::READY;
usb_set_interface(&usb_, iface_num_, 0);
return status;
}
UsbState::~UsbState() {}
zx_status_t UsbState::Allocate(uint64_t size, int ep_type) {
if (streaming_state_ != StreamingState::STOPPED && streaming_state_ != StreamingState::READY) {
zxlogf(ERROR, "Failed to allocate usb requests because state != STOPPED or READY");
return ZX_ERR_BAD_STATE;
}
if (size == 0) {
return ZX_ERR_INVALID_ARGS;
}
send_req_size_ = size;
if (size <= allocated_req_size_) {
zxlogf(DEBUG, "reusing allocated usb requests. had %lu, needed %lu", allocated_req_size_, size);
// Can reuse existing usb requests. Update the length field to the current size:
for (UsbRequest& req : allocated_requests_) {
req.req()->header.length = send_req_size_;
}
return ZX_OK;
}
// Need to allocate new usb requests, release any existing ones.
ZX_DEBUG_ASSERT(free_requests_.size() == allocated_requests_.size());
allocated_requests_.clear();
free_requests_.clear();
// If we are doing isochronous streaming, we can prevent multiple re-allocations by just
// allocating to the maximum request size. This won't usually cause unnecessary memory
// allocation because the requests are actually backed by vmos that are rounded up to
// the page size. Also this is what the previous version of the driver did...
// So, if this is the first time allocating requests for an isochronous stream, allocate
// the maximum bandwidth instead.
if ((ep_type == USB_ENDPOINT_ISOCHRONOUS) && (allocated_req_size_ == 0)) {
for (const auto& setting : streaming_settings_.endpoint_settings) {
if (setting.isoc_bandwidth > size) {
size = setting.isoc_bandwidth;
}
}
}
zxlogf(DEBUG, "allocating %d usb requests of size %lu", MAX_OUTSTANDING_REQS, size);
for (uint32_t i = 0; i < MAX_OUTSTANDING_REQS; i++) {
zx::result<UsbRequest> req_or = UsbRequest::Create(&usb_, usb_ep_addr_, size);
if (req_or.is_error()) {
zxlogf(ERROR, "usb_request_alloc failed: %d", req_or.error_value());
allocated_requests_.clear();
return req_or.error_value();
}
allocated_requests_.push_back(std::move(*req_or));
free_requests_.push_back(allocated_requests_.back().req());
zxlogf(DEBUG, "adding allocated requests... %lu ", allocated_requests_.size());
}
allocated_req_size_ = size;
return ZX_OK;
}
zx_status_t UsbState::SetFormat(fuchsia::camera::VideoFormat video_format) {
fbl::AutoLock lock(&lock_);
// We should never receive commands while in the STOPPING state.
if (streaming_state_ == StreamingState::STOPPING) {
zxlogf(ERROR, "SetFormat called while while UsbState is in temporary Stopping state.");
return ZX_ERR_BAD_STATE;
}
// We do not allow changing formats while streaming.
if (streaming_state_ == StreamingState::STREAMING) {
zxlogf(ERROR, "cannot set video format while streaming is not stopped");
return ZX_ERR_BAD_STATE;
}
// Convert from the client's video format proto to the device driver format
// and frame descriptors.
auto match = std::find_if(streaming_settings_.formats.begin(), streaming_settings_.formats.end(),
[video_format](const UvcFormat& f) { return f == video_format; });
if (match == streaming_settings_.formats.end()) {
zxlogf(ERROR, "could not find a mapping for the requested format");
return ZX_ERR_NOT_FOUND;
}
// Now push the settings to the usb device:
zxlogf(DEBUG, "trying format %u, frame desc %u", match->format_index, match->frame_index);
auto negotiation_result = ProbeAndCommit(&usb_, iface_num_, match->format_index,
match->frame_index, match->default_frame_interval);
if (negotiation_result.is_error()) {
return negotiation_result.error_value();
}
// Now, find a stream setting that will support the config we just committed.
// TODO(jocelyndang): we should calculate this ourselves instead
// of reading the reported value, as it is incorrect in some devices.
uint32_t required_bandwidth = negotiation_result->dwMaxPayloadTransferSize;
const StreamingEndpointSetting* best_setting = nullptr;
// Find a setting that supports the required bandwidth.
for (const auto& setting : streaming_settings_.endpoint_settings) {
// For bulk transfers, we use the first (and only) setting.
if (setting.ep_type == USB_ENDPOINT_BULK || setting.isoc_bandwidth >= required_bandwidth) {
best_setting = &setting;
break;
}
}
if (!best_setting) {
zxlogf(ERROR, "could not find a setting with bandwidth >= %u", required_bandwidth);
return ZX_ERR_NOT_SUPPORTED;
}
uint64_t allocate_req_size = best_setting->isoc_bandwidth;
if (best_setting->ep_type == USB_ENDPOINT_BULK) {
allocate_req_size =
std::min(usb_get_max_transfer_size(&usb_, usb_ep_addr_),
static_cast<uint64_t>(negotiation_result->dwMaxPayloadTransferSize));
}
auto status = Allocate(allocate_req_size, best_setting->ep_type);
if (status == ZX_OK) {
// We're now configured for a specific setting, save that:
alt_setting_ = best_setting->alt_setting;
negotiation_result_ = *negotiation_result;
ep_type_ = best_setting->ep_type;
// dwClockFrequency is an optional field of the negotiation.
// Use it if present.
if (negotiation_result->dwClockFrequency != 0) {
dev_clock_frequency_hz_ = negotiation_result->dwClockFrequency;
}
streaming_state_ = StreamingState::READY;
}
return status;
}
// when we get a request, send it to the callback and put it back in the queue:
void UsbState::RequestComplete(usb_request_t* req) {
fbl::AutoLock lock(&lock_);
// Check if we are in the stopping state. If so, do not send updates or re-queue
// the requests.
if (streaming_state_ == StreamingState::STOPPING) {
zxlogf(DEBUG, "RequestComplete: status: %s", zx_status_get_string(req->response.status));
free_requests_.push_back(req);
zxlogf(DEBUG, "Draining requests: %lu / %lu", free_requests_.size(),
allocated_requests_.size());
} else {
// The only other legal state is to be currently streaming.
ZX_ASSERT_MSG(streaming_state_ == StreamingState::STREAMING,
"Received request callback, but stream was not running! "
"Something is wrong with the state machine!!");
// We call the callback while holding the lock. We therefore place the restriction that
// none of our methods can be called from within this callback.
req_callback_(req);
usb_request_queue(&usb_, req, &req_complete_callback_);
}
}
} // namespace camera::usb_video