root/src/add-ons/kernel/busses/scsi/virtio/VirtioSCSIController.cpp
/*
 * Copyright 2013, Jérôme Duval, korli@users.berlios.de.
 * Distributed under the terms of the MIT License.
 */


#include "VirtioSCSIPrivate.h"

#include <StackOrHeapArray.h>

#include <new>
#include <stdlib.h>
#include <strings.h>

#include <util/AutoLock.h>


const char *
get_feature_name(uint64 feature)
{
        switch (feature) {
                case VIRTIO_SCSI_F_INOUT:
                        return "in out";
                case VIRTIO_SCSI_F_HOTPLUG:
                        return "hotplug";
                case VIRTIO_SCSI_F_CHANGE:
                        return "change";
        }
        return NULL;
}


VirtioSCSIController::VirtioSCSIController(device_node *node)
        :
        fNode(node),
        fVirtio(NULL),
        fVirtioDevice(NULL),
        fStatus(B_NO_INIT),
        fRequest(NULL),
        fCurrentRequest(0),
        fEventDPC(NULL)
{
        CALLED();

        fInterruptCondition.Init(this, "virtio scsi transfer");

        if (gSCSI->alloc_dpc(&fEventDPC) != B_OK)
                return;

        // get the Virtio device from our parent's parent
        device_node *parent = gDeviceManager->get_parent_node(node);
        device_node *virtioParent = gDeviceManager->get_parent_node(parent);
        gDeviceManager->put_node(parent);

        gDeviceManager->get_driver(virtioParent, (driver_module_info **)&fVirtio,
                (void **)&fVirtioDevice);
        gDeviceManager->put_node(virtioParent);

        fVirtio->negotiate_features(fVirtioDevice,
                VIRTIO_SCSI_F_CHANGE /*VIRTIO_SCSI_F_HOTPLUG*/,
                &fFeatures, &get_feature_name);

        fStatus = fVirtio->read_device_config(fVirtioDevice, 0, &fConfig,
                sizeof(struct virtio_scsi_config));
        if (fStatus != B_OK)
                return;

        fConfig.sense_size = VIRTIO_SCSI_SENSE_SIZE;
        fConfig.cdb_size = VIRTIO_SCSI_CDB_SIZE;

        fVirtio->write_device_config(fVirtioDevice,
                offsetof(struct virtio_scsi_config, sense_size), &fConfig.sense_size,
                sizeof(fConfig.sense_size));
        fVirtio->write_device_config(fVirtioDevice,
                offsetof(struct virtio_scsi_config, cdb_size), &fConfig.cdb_size,
                sizeof(fConfig.cdb_size));

        fRequest = new(std::nothrow) VirtioSCSIRequest(true);
        if (fRequest == NULL) {
                fStatus = B_NO_MEMORY;
                return;
        }

        ::virtio_queue virtioQueues[3];
        uint16 requestedSizes[3] = { 0, 0, 0 };
        requestedSizes[2] = fConfig.seg_max + 2;
                // two entries are taken up by the header and result

        fStatus = fVirtio->alloc_queues(fVirtioDevice, 3, virtioQueues,
                requestedSizes);
        if (fStatus != B_OK) {
                ERROR("queue allocation failed (%s)\n", strerror(fStatus));
                return;
        }

        fControlVirtioQueue = virtioQueues[0];
        fEventVirtioQueue = virtioQueues[1];
        fRequestVirtioQueue = virtioQueues[2];

        for (uint32 i = 0; i < VIRTIO_SCSI_NUM_EVENTS; i++)
                _SubmitEvent(i);

        fStatus = fVirtio->setup_interrupt(fVirtioDevice, NULL, this);
        if (fStatus != B_OK) {
                ERROR("interrupt setup failed (%s)\n", strerror(fStatus));
                return;
        }

        fStatus = fVirtio->queue_setup_interrupt(fControlVirtioQueue,
                NULL, NULL);
        if (fStatus == B_OK) {
                fStatus = fVirtio->queue_setup_interrupt(fEventVirtioQueue,
                        VirtioSCSIController::_EventCallback, this);
        }
        if (fStatus == B_OK) {
                fStatus = fVirtio->queue_setup_interrupt(fRequestVirtioQueue,
                        VirtioSCSIController::_RequestCallback, this);
        }

        if (fStatus != B_OK) {
                ERROR("queue interrupt setup failed (%s)\n", strerror(fStatus));
                return;
        }
}


VirtioSCSIController::~VirtioSCSIController()
{
        CALLED();
        delete fRequest;

        gSCSI->free_dpc(fEventDPC);
}


status_t
VirtioSCSIController::InitCheck()
{
        return fStatus;
}


void
VirtioSCSIController::SetBus(scsi_bus bus)
{
        fBus = bus;
}


void
VirtioSCSIController::PathInquiry(scsi_path_inquiry *info)
{
        info->hba_inquiry = SCSI_PI_TAG_ABLE;
        info->hba_misc = 0;
        info->initiator_id = VIRTIO_SCSI_INITIATOR_ID;
        info->hba_queue_size = fConfig.cmd_per_lun != 0 ? fConfig.cmd_per_lun : 1;
        memset(info->vuhba_flags, 0, sizeof(info->vuhba_flags));

        strlcpy(info->sim_vid, "Haiku", SCSI_SIM_ID);
        strlcpy(info->hba_vid, "VirtIO", SCSI_HBA_ID);

        strlcpy(info->sim_version, "1.0", SCSI_VERS);
        strlcpy(info->hba_version, "1.0", SCSI_VERS);
        strlcpy(info->controller_family, "Virtio", SCSI_FAM_ID);
        strlcpy(info->controller_type, "Virtio", SCSI_TYPE_ID);
}


void
VirtioSCSIController::GetRestrictions(uint8 targetID, bool *isATAPI,
        bool *noAutoSense, uint32 *maxBlocks)
{
        *isATAPI = false;
        *noAutoSense = true;
        *maxBlocks = fConfig.cmd_per_lun;
}


uchar
VirtioSCSIController::ResetDevice(uchar targetID, uchar targetLUN)
{
        return SCSI_REQ_CMP;
}


status_t
VirtioSCSIController::ExecuteRequest(scsi_ccb *ccb)
{
        status_t result = fRequest->Start(ccb);
        if (result != B_OK)
                return result;

        if (ccb->cdb[0] == SCSI_OP_REQUEST_SENSE && fRequest->HasSense()) {
                TRACE("request sense\n");
                fRequest->RequestSense();
                fRequest->Finish(false);
                return B_OK;
        }

        if (ccb->target_id > fConfig.max_target) {
                ERROR("invalid target device\n");
                fRequest->SetStatus(SCSI_TID_INVALID);
                fRequest->Finish(false);
                return B_BAD_INDEX;
        }

        if (ccb->target_lun > fConfig.max_lun) {
                ERROR("invalid lun device\n");
                fRequest->SetStatus(SCSI_LUN_INVALID);
                fRequest->Finish(false);
                return B_BAD_INDEX;
        }

        if (ccb->cdb_length > VIRTIO_SCSI_CDB_SIZE) {
                fRequest->SetStatus(SCSI_REQ_INVALID);
                fRequest->Finish(false);
                return B_BAD_VALUE;
        }

        bool isOut = (ccb->flags & SCSI_DIR_MASK) == SCSI_DIR_OUT;
        bool isIn = (ccb->flags & SCSI_DIR_MASK) == SCSI_DIR_IN;

        // TODO check feature inout if request is bidirectional

        fRequest->SetTimeout(ccb->timeout > 0 ? ccb->timeout * 1000 * 1000
                : VIRTIO_SCSI_STANDARD_TIMEOUT);

        uint32 inCount = (isIn ? ccb->sg_count : 0) + 1;
        uint32 outCount = (isOut ? ccb->sg_count : 0) + 1;
        BStackOrHeapArray<physical_entry, 16> entries(inCount + outCount);
        if (!entries.IsValid()) {
                fRequest->SetStatus(SCSI_REQ_INVALID);
                fRequest->Finish(false);
                return B_NO_MEMORY;
        }
        fRequest->FillRequest(inCount, outCount, entries);

        atomic_add(&fCurrentRequest, 1);
        ConditionVariableEntry entry;
        fInterruptCondition.Add(&entry);

        result = fVirtio->queue_request_v(fRequestVirtioQueue, entries,
                outCount, inCount, (void *)(addr_t)fCurrentRequest);

        if (result != B_OK)
                ERROR("queueing failed with status: %#" B_PRIx32 "\n", result);
        else {
                result = entry.Wait(B_RELATIVE_TIMEOUT, fRequest->Timeout());
                if (result != B_OK)
                        ERROR("wait failed with status: %#" B_PRIx32 "\n", result);
        }

        if (result != B_OK) {
                fRequest->Abort();
                return result;
        }

        return fRequest->Finish(false);
}


uchar
VirtioSCSIController::AbortRequest(scsi_ccb *request)
{
        return SCSI_REQ_CMP;
}


uchar
VirtioSCSIController::TerminateRequest(scsi_ccb *request)
{
        return SCSI_REQ_CMP;
}


status_t
VirtioSCSIController::Control(uint8 targetID, uint32 op, void *buffer,
        size_t length)
{
        CALLED();
        return B_DEV_INVALID_IOCTL;
}


void
VirtioSCSIController::_RequestCallback(void* driverCookie, void* cookie)
{
        CALLED();
        VirtioSCSIController* controller = (VirtioSCSIController*)driverCookie;
        controller->_RequestInterrupt();
}


void
VirtioSCSIController::_RequestInterrupt()
{
        void* cookie = NULL;
        while (fVirtio->queue_dequeue(fRequestVirtioQueue, &cookie, NULL)) {
                if ((int32)(addr_t)cookie == atomic_get(&fCurrentRequest))
                        fInterruptCondition.NotifyAll();
        }
}



void
VirtioSCSIController::_EventCallback(void* driverCookie, void* cookie)
{
        CALLED();
        VirtioSCSIController* controller = (VirtioSCSIController*)driverCookie;

        virtio_scsi_event* event = NULL;
        while (controller->fVirtio->queue_dequeue(controller->fEventVirtioQueue,
                        (void**)&event, NULL)) {
                controller->_EventInterrupt(event);
        }
}


void
VirtioSCSIController::_EventInterrupt(struct virtio_scsi_event* event)
{
        CALLED();
        TRACE("events %#x\n", event->event);
        if ((event->event & VIRTIO_SCSI_T_EVENTS_MISSED) != 0) {
                ERROR("events missed\n");
        } else switch (event->event) {
                case VIRTIO_SCSI_T_TRANSPORT_RESET:
                        ERROR("transport reset\n");
                        break;
                case VIRTIO_SCSI_T_ASYNC_NOTIFY:
                        ERROR("async notify\n");
                        break;
                case VIRTIO_SCSI_T_PARAM_CHANGE:
                {
                        uint16 sense = (event->reason >> 8)
                                | ((event->reason & 0xff) << 8);
                        if (sense == SCSIS_ASC_CAPACITY_DATA_HAS_CHANGED) {
                                ERROR("capacity data has changed for %x:%x\n", event->lun[1],
                                        event->lun[2] << 8 | event->lun[3]);
                                gSCSI->schedule_dpc(fBus, fEventDPC, _RescanChildBus, this);
                        } else
                                ERROR("param change, unknown reason\n");
                        break;
                }
                default:
                        ERROR("unknown event %#x\n", event->event);
                        break;
        }
}


void
VirtioSCSIController::_SubmitEvent(uint32 eventNumber)
{
        CALLED();
        struct virtio_scsi_event* event = &fEventBuffers[eventNumber];
        bzero(event, sizeof(struct virtio_scsi_event));

        physical_entry entry;
        get_memory_map(event, sizeof(struct virtio_scsi_event), &entry, 1);

        fVirtio->queue_request_v(fEventVirtioQueue, &entry,
                0, 1, event);
}


void
VirtioSCSIController::_RescanChildBus(void *cookie)
{
        CALLED();
        VirtioSCSIController* controller = (VirtioSCSIController*)cookie;
        device_node *childNode = NULL;
        const device_attr attrs[] = { { NULL } };
        if (gDeviceManager->get_next_child_node(controller->fNode, attrs,
                &childNode) != B_OK) {
                ERROR("couldn't find the child node for %p\n", controller->fNode);
                return;
        }

        gDeviceManager->rescan_node(childNode);
        TRACE("rescan done %p\n", childNode);
        gDeviceManager->put_node(childNode);
}