// -*- Mode: C++ -*-
// vim:ft=cpp
/**
 * \file
 */
/*
 * (c) 2014 Alexander Warg <alexander.warg@kernkonzept.com>
 *
 * License: see LICENSE.spdx (in this directory or the directories above)
 */

#pragma once

#include <l4/sys/types.h>
#include <l4/sys/l4int.h>
#include <l4/sys/capability>
#include <l4/re/dataspace>
#include <l4/re/protocols.h>
#include <l4/sys/cxx/types>
#include <l4/sys/cxx/ipc_types>
#include <l4/sys/cxx/ipc_iface>

namespace L4Re
{


/**
 * Managed DMA Address Space.
 *
 * A managed Dma_space represents the L4Re abstraction of an DMA address space
 * of one or several devices. Devices are assigned to a managed Dma_space by
 * binding the Dma_space to the respective DMA domain (see
 * L4vbus::Vbus::assign_dma_domain()), which might link the Dma_space with a
 * kernel \ref l4_kernel_object_dmar_space. Note that several DMA domains can
 * be bound to the same Dma_space. Whenever a device needs direct access to
 * parts of an L4Re::Dataspace, that part of the data space must be mapped to
 * the managed Dma_space that is assigned to that device. Binding to DMA
 * domains must happen before mapping. After the DMA accesses to the memory
 * are finished the memory must be unmapped from the device's DMA address
 * space.
 *
 * Mapping to a managed DMA address space, using map(), makes the given parts
 * of the data space visible to the associated device at the returned DMA
 * address. As long as the memory is mapped into a DMA space it is 'pinned'
 * and cannot be subject to dynamic memory management such as swapping.
 *
 * unmap() is the reverse operation to map() and unmaps the given
 * data-space part for the DMA address space.
 */
class Dma_space :
  public L4::Kobject_0t< Dma_space,
                         L4RE_PROTO_DMA_SPACE,
                         L4::Type_info::Demand_t<1> >
{
public:
  /// Data type for DMA addresses.
  typedef l4_uint64_t Dma_addr;
  typedef l4_uint64_t Dma_size;

  /**
   * Direction of the DMA transfers
   *
   * \deprecated Ignored by the API. Use function overloads without the
   *             direction parameter instead.
   */
  enum Direction
  {
    Bidirectional, ///< device reads and writes to the memory
    To_device,     ///< device reads the memory
    From_device,   ///< device writes to the memory
    None           ///< device is coherently connected to the memory
  };

  /**
   * Attributes used for the memory region.
   * \sa Attributes
   */
  enum Attribute : unsigned
  {
    /**
     * Search for suitable address.
     *
     * A spot for the whole mapping is searched, starting from the passed start
     * address. Without this flag, the start address is taken literally. Can be
     * combined with Attribute::Partial_map.
     *
     * If no address remapping is possible (i.e., the Dma_space was associated
     * by Dma_space_mgr::associate_phys()), the flag has no effect. The
     * Dma_space::map() call will always return the physical address.
     */
    Search_addr = 1U << 0,

    /**
     * Truncate mapping if it does not fit.
     *
     * If there is not enough room after the chosen start address, truncate the
     * mapping instead of failing.
     */
    Partial_map = 1U << 1,

    /**
     * Reserve a region instead of mapping pages.
     *
     * If set, the dataspace argument of Dma_space::map() is ignored. Use the
     * Attribute::Replace flag to place dataspace mappings in a reserved region
     * later. The reservation is released with Dma_space::unmap() carrying
     * Unmap_flag::Cancel_reservation.
     *
     * If no address remapping is possible (i.e., the Dma_space was associated
     * by Dma_space_mgr::associate_phys()), Dma_space::map() will always fail
     * with -L4_EPERM if this flag is present.
     */
    Reserve = 1U << 2,

    /**
     * Replace mappings.
     *
     * The pages in the map region are replaced with mappings from the
     * dataspace. Without this flag, existing mappings cause Dma_space::map()
     * to fail. Areas previously claimed by Dma_space_mgr::block_area() are
     * never replaced and still cause Dma_space::map() to fail.
     *
     * If combined with Attribute::Reserve, no dataspace mapping is
     * established. Instead, the reservation reference count is increased. Only
     * after the matching number of Dma_space::unmap(Cancel_reservation) calls,
     * the reservation is released.
     *
     * If no address remapping is possible (i.e., the Dma_space was associated
     * by Dma_space_mgr::associate_phys()), the flag has no effect. The
     * Dma_space::map() call will always return the physical address.
     *
     * Cannot be combined with Attribute::Search_addr or Attribute::Partial_map.
     */
    Replace = 1U << 3,
  };

  friend void enum_bitops_enable(Attribute);

  /**
   * Attributes for DMA mappings.
   *
   * \sa Attribute
   */
  using Attributes = L4::Types::Flags_t<Attribute, unsigned>;

  /**
   * Map the given part of this data space into the DMA address space.
   *
   * A suitable address in the DMA task will be chosen automatically.
   *
   * \param[in]     src      Source data space (that describes the memory).
   * \param[in]     offset   The offset (bytes) within `src`.
   * \param[in,out] size     The size (bytes) of the region to be mapped
   *                         for DMA, after successful mapping the size
   *                         returned is the size mapped for DMA as a single
   *                         block. This size might be smaller than the
   *                         original input size, in this case the caller might
   *                         call map() again with a new offset and the
   *                         remaining size.
   * \param[in]     attrs    The attributes used for this DMA mapping
   *                         (a combination of Dma_space::Attribute values).
   *                         The Search_addr attribute is forced by this
   *                         overload!
   * \param[in]     dir      The direction of the DMA transfer issued with
   *                         this mapping. The same value must later be passed
   *                         to unmap(). (ignored)
   * \param[out]    dma_addr The DMA address to use for DMA with the associated
   *                         device.
   *
   * \retval L4_EOK             Operation successful.
   * \retval -L4_EADDRNOTAVAIL  The specified region overlaps an existing
   *                            mapping.
   * \retval -L4_EINVAL         The capability `src` is invalid or does not
   *                            refer to a valid dataspace.
   * \retval -L4_ENOMEM         Not enough memory to allocate internal
   *                            datastructures.
   * \retval -L4_ERANGE         `offset` is larger than the size of the
   *                            dataspace.
   *
   * The DMA mapping is created writable if capability `src` has the permission
   * #L4_CAP_FPAGE_W. Otherwise, a read-only DMA mapping is established.
   *
   * \note Dma_space_mgr::associate() or Dma_space_mgr::associate_phys() must
   * be called prior to mapping memory.  Usually this is done implicitly when
   * binding the managed Dma_space to a DMA domain (see
   * L4vbus::Vbus::assign_dma_domain()).
   *
   * \deprecated Use the other overload without `dir` parameter.
   */
  l4_ret_t map(L4::Ipc::Cap<L4Re::Dataspace> src,
               L4Re::Dataspace::Offset offset,
               l4_size_t * size,
               Attributes attrs, Direction dir,
               Dma_addr *dma_addr)
  {
    static_cast<void>(dir);

    Dma_addr a = 0;
    Dma_size sz = *size;

    l4_ret_t ret = map(src, offset, &sz, &a, -1, attrs | Search_addr);
    if (ret >= 0)
      {
        *dma_addr = a;
        *size = sz;
      }

    return ret;
  }

  /**
   * Map the given part of this data space into the DMA address space.
   *
   * \param[in]     src      Source data space (that describes the memory).
   * \param[in]     offset   The offset (bytes) within `src`. Must be zero in
   *                         case Attribute::Reserve is passed in `attrs`.
   * \param[in,out] size     The size (bytes) of the region to be mapped
   *                         for DMA, after successful mapping the size
   *                         returned is the size mapped for DMA as a single
   *                         block. This size might be smaller than the
   *                         original input size, in this case the caller might
   *                         call map() again with a new offset and the
   *                         remaining size.
   * \param[in,out] dma_addr The DMA address to use for DMA with the associated
   *                         device. Either taken literally (in which case it
   *                         must be page aligned) or used as starting
   *                         address if Attribute::Search_addr is passed.
   * \param[in]     dma_max  Highest allowed DMA address of the mapping
   *                         (inclusive).
   * \param[in]     attrs    The attributes used for this DMA mapping
   *                         (a combination of Dma_space::Attribute values).
   * \param[in]     align    Alignment of mapping (log2 based). Only used
   *                         if Attribute::Search_addr is passed.
   *
   * \retval L4_EOK             Operation successful.
   * \retval -L4_EADDRNOTAVAIL  The specified region overlaps an existing
   *                            mapping/blocking or no space left in search
   *                            area.
   * \retval -L4_EPERM          Insufficient permissions; see precondition.
   * \retval -L4_EINVAL         The capability `src` is invalid or does not
   *                            refer to a valid dataspace.
   * \retval -L4_ENOMEM         Not enough memory to allocate internal
   *                            datastructures.
   * \retval -L4_ERANGE         `offset` is larger than the size of the
   *                            dataspace.
   *
   * The DMA mapping is created writable if capability `src` has the permission
   * #L4_CAP_FPAGE_W. Otherwise, a read-only DMA mapping is established.
   *
   * If no address remapping is possible (i.e., the Dma_space was associated
   * by Dma_space_mgr::associate_phys()), Dma_space::map() will always return
   * the physical address. The caller should therefore be prepared that a
   * desired DMA address is ignored.
   *
   * \note Dma_space_mgr::associate() or Dma_space_mgr::associate_phys() must
   * be called prior to mapping memory.  Usually this is done implicitly when
   * binding the managed Dma_space to a DMA domain (see
   * L4vbus::Vbus::assign_dma_domain()).
   */
  l4_ret_t map(L4::Ipc::Cap<L4Re::Dataspace> src,
               L4Re::Dataspace::Offset offset,
               Dma_size *size,
               Dma_addr *dma_addr,
               Dma_addr dma_max = -1,
               Attributes attrs = Search_addr,
               unsigned char align = L4_SUPERPAGESHIFT)
  {
    return map_t::call(c(), src, offset, size, align, attrs, dma_addr, dma_max);
  }

  L4_RPC_NF(
      l4_ret_t, map, (L4::Ipc::Opt<L4::Ipc::Cap<L4Re::Dataspace>> src,
                      L4Re::Dataspace::Offset offset,
                      L4::Ipc::In_out<Dma_size *> size, unsigned char align,
                      Attributes attrs, L4::Ipc::In_out<Dma_addr *> dma_addr,
                      Dma_addr dma_max));

  /**
   * Flags used by Dma_space::unmap().
   * \sa Unmap_flags
   */
  enum Unmap_flag : unsigned
  {
    /**
     * Cancel a prior Attribute::Reserve claim instead of unmapping a
     * dataspace mapping.
     *
     * Without this flag, unmap() releases one dataspace mapping that was
     * previously established by map() (with or without Attribute::Replace).
     * With this flag, unmap() releases one previous Attribute::Reserve
     * claim and does not touch any active dataspace mappings in the range.
     */
    Cancel_reservation = 1U << 0,
  };

  friend void enum_bitops_enable(Unmap_flag);

  /**
   * Flags for Dma_space::unmap().
   *
   * \sa Unmap_flag
   */
  using Unmap_flags = L4::Types::Flags_t<Unmap_flag, unsigned>;

  /**
   * Unmap the given part of this data space from the DMA address space.
   *
   * The calls to map() and unmap() must be balanced. That is, each page of
   * a mapping that was established by `map()` previously must be unmapped
   * exactly once.
   *
   * Sub-ranges that do not currently hold a mapping are silently skipped.
   * That is, unmapping the same region too often, is not treated as an error.
   *
   * \param dma_addr  The DMA address (returned by Dma_space::map()).
   * \param size      The size (bytes) of the memory region to unmap.
   * \param attrs     The attributes for the unmap (currently none, ignored).
   * \param dir       The direction of the finished DMA operation (ignored).
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_ENOMEM  Not enough memory to allocate internal datastructures.
   *
   * \deprecated Use the overload without `attrs` and `dir` because they are
   *             ignored.
   */
  l4_ret_t unmap(Dma_addr dma_addr, Dma_size size, Attributes attrs,
                 Direction dir)
  {
    static_cast<void>(attrs);
    static_cast<void>(dir);
    return unmap(dma_addr, size);
  }

  /**
   * Unmap the given part of this data space from the DMA address space.
   *
   * The calls to map() and unmap() must be balanced. That is, each page of
   * a mapping that was established by `map()` previously must be unmapped
   * exactly once. Likewise, every Attribute::Reserve claim must be released by
   * exactly one unmap() call carrying Unmap_flag::Cancel_reservation.
   *
   * Sub-ranges that do not currently hold a claim of the requested kind are
   * silently skipped. That is, unmapping the same region too often, is not
   * treated as an error. This behaviour applies both to actual mappings, as
   * well as reservations.
   *
   * \param dma_addr  The DMA address (returned by Dma_space::map()).
   * \param size      The size (bytes) of the memory region to unmap.
   * \param flags     A combination of Dma_space::Unmap_flag values.
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_EPERM   The range overlaps with a privileged
   *                     Dma_space_mgr::block_area() claim.
   * \retval -L4_EINVAL  Invalid arguments. Can be caused by unknown `flags`,
   *                     size of 0 or an address overflow.
   * \retval -L4_ENOMEM  Not enough memory to allocate internal datastructures.
   */
  l4_ret_t unmap(Dma_addr dma_addr, Dma_size size,
                 Unmap_flags flags = Unmap_flags())
  {
    return unmap_t::call(c(), dma_addr, size, flags);
  }

  L4_RPC_NF(l4_ret_t, unmap, (Dma_addr dma_addr, Dma_size size,
                              Unmap_flags flags));

  typedef L4::Typeid::Rpcs<map_t, unmap_t> Rpcs;
};

/**
 * A privileged interface to manage Dma_space associations for the clients.
 *
 * This interface is provided as a singleton by Moe.
 */
class Dma_space_mgr :
  public L4::Kobject_0t< Dma_space_mgr, L4::PROTO_ANY,
                         L4::Type_info::Demand_t<2> >
{
public:
  using Dma_addr = Dma_space::Dma_addr;
  using Dma_size = Dma_space::Dma_size;

  /**
   * Attributes assigned to the DMA space when associated with a
   * specific device.
   * \sa Space_attribs
   */
  enum Space_attrib : unsigned
  {
    /**
     * Request identity mappings.
     *
     * This is needed if some devices are not connected to an IOMMU and need
     * physical addresses.
     */
    Identity_map = 1U << 0,
  };

  friend void enum_bitops_enable(Space_attrib);

  /// Attributes used when associating the DMA space.
  using Space_attribs = L4::Types::Flags_t<Space_attrib, unsigned>;

  /**
   * Attributes used when blocking an area.
   * \sa Block_flags
   */
  enum Block_flag : unsigned
  {
    /**
     * Search suitable address.
     *
     * With this flag, the start address is taken as a lower bound. Without
     * the flag, `start` and `size` are taken literally.
     */
    Search_addr = 1U << 0,
  };

  friend void enum_bitops_enable(Block_flag);

  /// Attributes used when blocking an area.
  using Block_flags = L4::Types::Flags_t<Block_flag, unsigned>;

  /**
   * Associate a (kernel) \ref l4_kernel_object_dmar_space for a device to
   * a Dma_space.
   *
   * \param[in]  dma_space The Dma_space to associate the (kernel) \ref
   *                       l4_kernel_object_dmar_space with.
   * \param[in]  dma_task  The (kernel) \ref l4_kernel_object_dmar_space used
   *                       for the device that shall be associated with this
   *                       DMA space.
   * \param[in]  attr      Attributes for this DMA space. See
   *                       L4Re::Dma_space::Space_attrib.
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_EBUSY   The Dma_space is already associated.
   * \retval -L4_EINVAL  Invalid arguments, e.g. no `dma_task` capability
   *                     passed. In particular, when Space_attrib::Identity_map
   *                     is requested, the Dma_space must not have any
   *                     pre-existing blockings (see
   *                     Dma_space_mgr::block_area()) and must not have custom
   *                     address limits set (see Dma_space_mgr::set_limits()).
   * \retval -L4_ENOENT  Invalid `dma_space` was passed.
   * \retval -L4_ENOMEM  Not enough memory to allocate internal datastructures.
   * \retval -L4_EPERM   Insufficient permissions; see precondition.
   *
   * \pre The `dma_space` and `dma_task` capabilities must have the permission
   *      #L4_CAP_FPAGE_W.
   */
  L4_INLINE_RPC(
      l4_ret_t, associate, (L4::Ipc::Cap<Dma_space> dma_space,
                            L4::Ipc::Cap<L4::Task> dma_task,
                            Space_attribs attr));

  /**
   * Register a Dma_space to use for DMA.
   *
   * This function should be used, in case no IOMMU is present or configured.
   * The CPU's physical memory is used as DMA address space.
   *
   * \param[in]  dma_space  The Dma_space to register.
   * \param[in]  attr       Attributes for the provided DMA space. Currently
   *                        not used; must be the default-constructed
   *                        Space_attribs() value.
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_EBUSY   The Dma_space is already associated.
   * \retval -L4_EINVAL  Invalid arguments, for example a non-zero `attr`,
   *                     pre-existing blockings on `dma_space` (see
   *                     Dma_space_mgr::block_area()), or custom address
   *                     limits set on `dma_space` (see
   *                     Dma_space_mgr::set_limits()).
   * \retval -L4_ENOENT  Invalid `dma_space` was passed.
   * \retval -L4_ENOMEM  Not enough memory to allocate internal datastructures.
   * \retval -L4_EPERM   Insufficient permissions; see precondition.
   *
   * \pre The `dma_space` capability must have the permission #L4_CAP_FPAGE_W.
   */
  L4_INLINE_RPC(
      l4_ret_t, associate_phys, (L4::Ipc::Cap<Dma_space> dma_space,
                                 Space_attribs attr));

  /**
   * Disassociate the (kernel) \ref l4_kernel_object_dmar_space from this
   * Dma_space.
   *
   * \param[in]  dma_space  The Dma_space to de-register.
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_ENOENT  Invalid `dma_space` was passed or disassociate()
   *                     called without prior calling
   *                     associate()/associate_phys().
   * \retval -L4_EPERM   Insufficient permissions; see precondition.
   *
   * \attention This will remove all mappings, reservations and blockings
   *            from `dma_space`.
   *
   * \pre The `dma_space` capability must have the permission #L4_CAP_FPAGE_W.
   */
  L4_INLINE_RPC(
      l4_ret_t, disassociate, (L4::Ipc::Cap<Dma_space> dma_space));

  /**
   * Block an area in a Dma_space.
   *
   * The blocked area cannot be used for mappings in the given `dma_space`
   * after the call. The call will fail if there are already mappings in
   * the requested range. It is allowed to add block areas before
   * associate() or associate_phys() has been called.
   *
   * Blocked areas are allowed to overlap.
   *
   * \param[in]     dma_space The Dma_space to add blocked areas.
   * \param[in,out] addr      The DMA address of the blocked area. Either taken
   *                          literally (in which case it must be page aligned)
   *                          or as start address if Block_flag::Search_addr is
   *                          passed.
   * \param[in]     size      Size of the blocked region. Must be page aligned.
   * \param[in]     max_addr  Highest allowed DMA address of the region
   *                          (inclusive).
   * \param[in]     flags     The attributes used for this blocking
   *                          (a combination of Dma_space_mgr::Block_flag
   *                          values).
   * \param[in]     align     The log2 alignment of the blocked area. Only used
   *                          if Block_flag::Search_addr is passed.
   *
   * \retval L4_EOK             Operation successful.
   * \retval -L4_EADDRNOTAVAIL  The specified region overlaps an existing
   *                            mapping or no space left for the blocking.
   * \retval -L4_EINVAL         Invalid address, alignment or flag passed.
   *                            Block_flag::Search_addr passed but `dma_space`
   *                            is not associated yet.
   * \retval -L4_EPERM          Dma_space is associated with physical DMA
   *                            domain or insufficient permissions; see
   *                            precondition.
   * \retval -L4_ENOMEM         There is not enough memory to allocate internal
   *                            data structures.
   *
   * \pre The `dma_space` capability must have the permission #L4_CAP_FPAGE_W.
   */
  l4_ret_t block_area(L4::Ipc::Cap<Dma_space>     dma_space,
                      L4::Ipc::In_out<Dma_addr *> addr,
                      Dma_size                    size,
                      Dma_addr                    max_addr = -1,
                      Block_flags                 flags = Block_flags(),
                      unsigned char               align = L4_SUPERPAGESHIFT)
  {
    return block_area_t::call(c(), dma_space, addr, size, max_addr, flags,
                              align);
  }

  L4_RPC_NF(
      l4_ret_t, block_area, (L4::Ipc::Cap<Dma_space>      dma_space,
                             L4::Ipc::In_out<Dma_addr *>  addr,
                             Dma_size                     size,
                             Dma_addr                     max_addr,
                             Block_flags                  flags,
                             unsigned char                align));

  /**
   * Set lower and upper bound of DMA addresses.
   *
   * Mappings and blockings are only allowed in the given range.
   *
   * If multiple Dma_spaces share the same kernel
   * \ref l4_kernel_object_dmar_space (i.e., were associated by passing the
   * same `dma_task` to associate()), all DMA address allocations, reservations
   * and blockings performed through any of these Dma_spaces use the
   * intersection of all their individual limits. Tightening the limits on
   * one Dma_space therefore also constrains all other Dma_spaces sharing the
   * same DMA task; loosening the limits on one Dma_space has no effect
   * beyond what the most-restrictive peer Dma_space allows.
   *
   * \param[in]  dma_space  The Dma_space whose limits shall be changed.
   * \param[in]  min_addr   The lowest usable address in the Dma_space.
   * \param[in]  max_addr   The highest usable address in the Dma_space
   *                        (inclusive).
   *
   * \retval L4_EOK             Operation successful.
   * \retval -L4_EADDRNOTAVAIL  There are existing mappings or blocking outside
   *                            the bounds.
   * \retval -L4_EPERM          Dma_space is associated with physical DMA
   *                            domain or insufficient permissions; see
   *                            precondition.
   * \retval -L4_EINVAL         Invalid address passed.
   *
   * \pre The `dma_space` capability must have the permission #L4_CAP_FPAGE_W.
   */
  L4_INLINE_RPC(
      l4_ret_t, set_limits, (L4::Ipc::Cap<Dma_space> dma_space,
                             Dma_addr min_addr,
                             Dma_addr max_addr));


  typedef L4::Typeid::Rpcs<associate_t, associate_phys_t, disassociate_t,
                           block_area_t, set_limits_t> Rpcs;
};

}
