Program Listing for File aja_source.cpp
↰ Return to documentation for file (gxf_extensions/aja/aja_source.cpp)
            
            /*
* Copyright (c) 2022, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "aja_source.hpp"
#include <cuda.h>
#include <cuda_runtime.h>
#include <string>
#include <utility>
#include "gxf/multimedia/video.hpp"
template <>
struct YAML::convert<NTV2Channel> {
  static bool decode(const Node& node, NTV2Channel& rhs) {
    if (!node.IsScalar()) return false;
    const std::string prefix("NTV2_CHANNEL");
    auto value = node.Scalar();
    if (value.find(prefix) != 0) return false;
    value = value.substr(prefix.length());
    try {
      size_t len;
      const auto index = std::stoi(value, &len);
      if (index < 1 || index > NTV2_MAX_NUM_CHANNELS || len != value.length()) { return false; }
      rhs = static_cast<NTV2Channel>(index - 1);
      return true;
    } catch (...) { return false; }
  }
};
namespace nvidia {
namespace holoscan {
AJASource::AJASource() : pixel_format_(kDefaultPixelFormat), current_buffer_(0) {}
gxf_result_t AJASource::registerInterface(gxf::Registrar* registrar) {
  gxf::Expected<void> result;
  result &= registrar->parameter(signal_, "signal", "Output", "Output signal.");
  result &= registrar->parameter(device_specifier_, "device", "Device", "Device specifier.",
                                 std::string(kDefaultDevice));
  result &=
      registrar->parameter(channel_, "channel", "Channel", "NTV2Channel to use.", kDefaultChannel);
  result &= registrar->parameter(width_, "width", "Width", "Width of the stream.", kDefaultWidth);
  result &=
      registrar->parameter(height_, "height", "Height", "Height of the stream.", kDefaultHeight);
  result &= registrar->parameter(framerate_, "framerate", "Framerate", "Framerate of the stream.",
                                 kDefaultFramerate);
  result &= registrar->parameter(use_rdma_, "rdma", "RDMA", "Enable RDMA.", kDefaultRDMA);
  return gxf::ToResultCode(result);
}
AJAStatus AJASource::DetermineVideoFormat() {
  if (width_ == 1920 && height_ == 1080 && framerate_ == 60) {
    video_format_ = NTV2_FORMAT_1080p_6000_A;
  } else {
    return AJA_STATUS_UNSUPPORTED;
  }
  return AJA_STATUS_SUCCESS;
}
AJAStatus AJASource::OpenDevice() {
  // Get the requested device.
  if (!CNTV2DeviceScanner::GetFirstDeviceFromArgument(device_specifier_, device_)) {
    GXF_LOG_ERROR("Device %s not found.", device_specifier_.get().c_str());
    return AJA_STATUS_OPEN;
  }
  // Check if the device is ready.
  if (!device_.IsDeviceReady(false)) {
    GXF_LOG_ERROR("Device %s not ready.", device_specifier_.get().c_str());
    return AJA_STATUS_INITIALIZE;
  }
  // Get the device ID.
  device_id_ = device_.GetDeviceID();
  // Detect Kona HDMI device.
  is_kona_hdmi_ = NTV2DeviceGetNumHDMIVideoInputs(device_id_) > 1;
  // Check if a TSI 4x format is needed.
  if (is_kona_hdmi_) { use_tsi_ = GetNTV2VideoFormatTSI(&video_format_); }
  // Check device capabilities.
  if (!NTV2DeviceCanDoVideoFormat(device_id_, video_format_)) {
    GXF_LOG_ERROR("AJA device does not support requested video format.");
    return AJA_STATUS_UNSUPPORTED;
  }
  if (!NTV2DeviceCanDoFrameBufferFormat(device_id_, pixel_format_)) {
    GXF_LOG_ERROR("AJA device does not support requested pixel format.");
    return AJA_STATUS_UNSUPPORTED;
  }
  if (!NTV2DeviceCanDoCapture(device_id_)) {
    GXF_LOG_ERROR("AJA device cannot capture video.");
    return AJA_STATUS_UNSUPPORTED;
  }
  if (!NTV2_IS_VALID_CHANNEL(channel_)) {
    GXF_LOG_ERROR("Invalid AJA channel: %d", channel_);
    return AJA_STATUS_UNSUPPORTED;
  }
  return AJA_STATUS_SUCCESS;
}
AJAStatus AJASource::SetupVideo() {
  NTV2InputSourceKinds input_kind = is_kona_hdmi_ ? NTV2_INPUTSOURCES_HDMI : NTV2_INPUTSOURCES_SDI;
  NTV2InputSource input_src = ::NTV2ChannelToInputSource(channel_, input_kind);
  NTV2Channel tsi_channel = static_cast<NTV2Channel>(channel_ + 1);
  if (!IsRGBFormat(pixel_format_)) {
    GXF_LOG_ERROR("YUV formats not yet supported");
    return AJA_STATUS_UNSUPPORTED;
  }
  // Detect if the source is YUV or RGB (i.e. if CSC is required or not).
  bool is_input_rgb(false);
  if (input_kind == NTV2_INPUTSOURCES_HDMI) {
    NTV2LHIHDMIColorSpace input_color;
    device_.GetHDMIInputColor(input_color, channel_);
    is_input_rgb = (input_color == NTV2_LHIHDMIColorSpaceRGB);
  }
  // Setup the input routing.
  device_.ClearRouting();
  device_.EnableChannel(channel_);
  if (use_tsi_) {
    device_.SetTsiFrameEnable(true, channel_);
    device_.EnableChannel(tsi_channel);
  }
  device_.SetMode(channel_, NTV2_MODE_CAPTURE);
  if (NTV2DeviceHasBiDirectionalSDI(device_id_) && NTV2_INPUT_SOURCE_IS_SDI(input_src)) {
    device_.SetSDITransmitEnable(channel_, false);
  }
  device_.SetVideoFormat(video_format_, false, false, channel_);
  device_.SetFrameBufferFormat(channel_, pixel_format_);
  if (use_tsi_) { device_.SetFrameBufferFormat(tsi_channel, pixel_format_); }
  device_.EnableInputInterrupt(channel_);
  device_.SubscribeInputVerticalEvent(channel_);
  NTV2OutputXptID input_output_xpt =
      GetInputSourceOutputXpt(input_src, /*DS2*/ false, is_input_rgb, /*Quadrant*/ 0);
  NTV2InputXptID fb_input_xpt(GetFrameBufferInputXptFromChannel(channel_));
  if (use_tsi_) {
    if (!is_input_rgb) {
      if (NTV2DeviceGetNumCSCs(device_id_) < 4) {
        GXF_LOG_ERROR("CSCs not available for TSI input.");
        return AJA_STATUS_UNSUPPORTED;
      }
      device_.Connect(NTV2_XptFrameBuffer1Input, NTV2_Xpt425Mux1ARGB);
      device_.Connect(NTV2_XptFrameBuffer1BInput, NTV2_Xpt425Mux1BRGB);
      device_.Connect(NTV2_XptFrameBuffer2Input, NTV2_Xpt425Mux2ARGB);
      device_.Connect(NTV2_XptFrameBuffer2BInput, NTV2_Xpt425Mux2BRGB);
      device_.Connect(NTV2_Xpt425Mux1AInput, NTV2_XptCSC1VidRGB);
      device_.Connect(NTV2_Xpt425Mux1BInput, NTV2_XptCSC2VidRGB);
      device_.Connect(NTV2_Xpt425Mux2AInput, NTV2_XptCSC3VidRGB);
      device_.Connect(NTV2_Xpt425Mux2BInput, NTV2_XptCSC4VidRGB);
      device_.Connect(NTV2_XptCSC1VidInput, NTV2_XptHDMIIn1);
      device_.Connect(NTV2_XptCSC2VidInput, NTV2_XptHDMIIn1Q2);
      device_.Connect(NTV2_XptCSC3VidInput, NTV2_XptHDMIIn1Q3);
      device_.Connect(NTV2_XptCSC4VidInput, NTV2_XptHDMIIn1Q4);
    } else {
      device_.Connect(NTV2_XptFrameBuffer1Input, NTV2_Xpt425Mux1ARGB);
      device_.Connect(NTV2_XptFrameBuffer1BInput, NTV2_Xpt425Mux1BRGB);
      device_.Connect(NTV2_XptFrameBuffer2Input, NTV2_Xpt425Mux2ARGB);
      device_.Connect(NTV2_XptFrameBuffer2BInput, NTV2_Xpt425Mux2BRGB);
      device_.Connect(NTV2_Xpt425Mux1AInput, NTV2_XptHDMIIn1RGB);
      device_.Connect(NTV2_Xpt425Mux1BInput, NTV2_XptHDMIIn1Q2RGB);
      device_.Connect(NTV2_Xpt425Mux2AInput, NTV2_XptHDMIIn1Q3RGB);
      device_.Connect(NTV2_Xpt425Mux2BInput, NTV2_XptHDMIIn1Q4RGB);
    }
  } else if (!is_input_rgb) {
    if (NTV2DeviceGetNumCSCs(device_id_) <= static_cast<int>(channel_)) {
      GXF_LOG_ERROR("No CSC available for NTV2_CHANNEL%d", channel_ + 1);
      return AJA_STATUS_UNSUPPORTED;
    }
    NTV2InputXptID csc_input = GetCSCInputXptFromChannel(channel_);
    NTV2OutputXptID csc_output =
        GetCSCOutputXptFromChannel(channel_, /*inIsKey*/ false, /*inIsRGB*/ true);
    device_.Connect(fb_input_xpt, csc_output);
    device_.Connect(csc_input, input_output_xpt);
  } else {
    device_.Connect(fb_input_xpt, input_output_xpt);
  }
  // Wait for a number of frames to acquire video signal.
  current_hw_frame_ = 0;
  device_.SetInputFrame(channel_, current_hw_frame_);
  device_.WaitForInputVerticalInterrupt(channel_, kWarmupFrames);
  return AJA_STATUS_SUCCESS;
}
AJAStatus AJASource::SetupBuffers() {
  auto size = GetVideoWriteSize(video_format_, pixel_format_);
  buffers_.resize(kNumBuffers);
  for (auto& buf : buffers_) {
    if (use_rdma_) {
      cudaMalloc(&buf, size);
      unsigned int syncFlag = 1;
      if (cuPointerSetAttribute(&syncFlag, CU_POINTER_ATTRIBUTE_SYNC_MEMOPS,
                                reinterpret_cast<CUdeviceptr>(buf))) {
        GXF_LOG_ERROR("Failed to set SYNC_MEMOPS CUDA attribute for RDMA");
        return AJA_STATUS_INITIALIZE;
      }
    } else {
      buf = malloc(size);
    }
    if (!buf) {
      GXF_LOG_ERROR("Failed to allocate buffer memory");
      return AJA_STATUS_INITIALIZE;
    }
    if (!device_.DMABufferLock(static_cast<const ULWord*>(buf), size, true, use_rdma_)) {
      GXF_LOG_ERROR("Failed to map buffer for DMA");
      return AJA_STATUS_INITIALIZE;
    }
  }
  return AJA_STATUS_SUCCESS;
}
gxf_result_t AJASource::start() {
  GXF_LOG_INFO("AJA Source: Using NTV2_CHANNEL%d", (channel_.get() + 1));
  GXF_LOG_INFO("AJA Source: RDMA is %s", use_rdma_ ? "enabled" : "disabled");
  AJAStatus status = DetermineVideoFormat();
  if (AJA_FAILURE(status)) {
    GXF_LOG_ERROR("Video format could not be determined based on parameters.");
    return GXF_FAILURE;
  }
  status = OpenDevice();
  if (AJA_FAILURE(status)) {
    GXF_LOG_ERROR("Failed to open device %s", device_specifier_.get().c_str());
    return GXF_FAILURE;
  }
  status = SetupVideo();
  if (AJA_FAILURE(status)) {
    GXF_LOG_ERROR("Failed to setup device %s", device_specifier_.get().c_str());
    return GXF_FAILURE;
  }
  status = SetupBuffers();
  if (AJA_FAILURE(status)) {
    GXF_LOG_ERROR("Failed to setup AJA buffers.");
    return GXF_FAILURE;
  }
  return GXF_SUCCESS;
}
gxf_result_t AJASource::stop() {
  device_.UnsubscribeInputVerticalEvent(channel_);
  for (auto& buf : buffers_) {
    if (use_rdma_) {
      cudaFree(buf);
    } else {
      free(buf);
    }
  }
  buffers_.clear();
  return GXF_SUCCESS;
}
gxf_result_t AJASource::tick() {
  // Update the next input frame and wait until it starts.
  uint32_t next_hw_frame = (current_hw_frame_ + 1) % 2;
  device_.SetInputFrame(channel_, next_hw_frame);
  device_.WaitForInputVerticalInterrupt(channel_);
  // Read the last completed frame.
  auto size = GetVideoWriteSize(video_format_, pixel_format_);
  auto ptr = static_cast<ULWord*>(buffers_[current_buffer_]);
  device_.DMAReadFrame(current_hw_frame_, ptr, size);
  // Set the frame to read for the next tick.
  current_hw_frame_ = next_hw_frame;
  // Pass the frame downstream.
  auto message = gxf::Entity::New(context());
  if (!message) {
    GXF_LOG_ERROR("Failed to allocate message; terminating.");
    return GXF_FAILURE;
  }
  auto buffer = message.value().add<gxf::VideoBuffer>();
  if (!buffer) {
    GXF_LOG_ERROR("Failed to allocate video buffer; terminating.");
    return GXF_FAILURE;
  }
  gxf::VideoTypeTraits<gxf::VideoFormat::GXF_VIDEO_FORMAT_RGBA> video_type;
  gxf::VideoFormatSize<gxf::VideoFormat::GXF_VIDEO_FORMAT_RGBA> color_format;
  auto color_planes = color_format.getDefaultColorPlanes(width_, height_);
  gxf::VideoBufferInfo info{width_, height_, video_type.value, color_planes,
                            gxf::SurfaceLayout::GXF_SURFACE_LAYOUT_PITCH_LINEAR};
  auto storage_type = use_rdma_ ? gxf::MemoryStorageType::kDevice : gxf::MemoryStorageType::kHost;
  buffer.value()->wrapMemory(info, size, storage_type, buffers_[current_buffer_], nullptr);
  const auto result = signal_->publish(std::move(message.value()));
  current_buffer_ = (current_buffer_ + 1) % kNumBuffers;
  return gxf::ToResultCode(message);
}
bool AJASource::GetNTV2VideoFormatTSI(NTV2VideoFormat* format) {
  switch (*format) {
    case NTV2_FORMAT_3840x2160p_2400:
      *format = NTV2_FORMAT_4x1920x1080p_2400;
      return true;
    case NTV2_FORMAT_3840x2160p_6000:
      *format = NTV2_FORMAT_4x1920x1080p_6000;
      return true;
    case NTV2_FORMAT_4096x2160p_2400:
      *format = NTV2_FORMAT_4x2048x1080p_2400;
      return true;
    case NTV2_FORMAT_4096x2160p_6000:
      *format = NTV2_FORMAT_4x2048x1080p_6000;
      return true;
    default:
      return false;
  }
}
}  // namespace holoscan
}  // namespace nvidia