File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / virtio_scsi.cpp
Revision 1.1.1.2 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:05:59 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2024 nono project
// Licensed under nono-license.txt
//

//
// VirtIO SCSI デバイス
//

#include "virtio_scsi.h"
#include "virtio_def.h"
#include "mainbus.h"
#include "memorystream.h"
#include "monitor.h"
#include "scsidev.h"
#include "scsidomain.h"

// デバイス構成レイアウト
class VirtIOSCSIConfigWriter
{
 public:
	le32 num_queues {};
	le32 seg_max {};
	le32 max_sectors {};
	le32 cmd_per_lun {};
	le32 event_info_size {};
	le32 sense_size {};
	le32 cdb_size {};
	le16 max_channel {};
	le16 max_target {};
	le32 max_lun {};

 public:
	void WriteTo(uint8 *dst) const;
};

// コンストラクタ
VirtIOSCSIDevice::VirtIOSCSIDevice(uint slot_)
	: inherited(OBJ_VIRTIO_SCSI, slot_)
{
	// 割り込み名(とログ名)
	strlcpy(intrname, "VIOSCSI", sizeof(intrname));
	SetName(intrname);
	ClearAlias();
	AddAlias(intrname);

	device_id = VirtIO::DEVICE_ID_SCSI;
	vqueues.emplace_back(this, 0, "ControlQ", 8);
	vqueues.emplace_back(this, 1, "EventQ", 8);
	vqueues.emplace_back(this, 2, "RequestQ", 8);

	// 完了通知メッセージ
	msgid = MessageID::VIRTIO_SCSI_DONE;

	// モニタの行数。
	int vqlines = 0;
	for (const auto& q : vqueues) {
		vqlines += 7 + q.num_max;
	}

	monitor = gMonitorManager->Regist(ID_MONITOR_VIRTIO_SCSI, this);
	monitor->SetCallback(&VirtIOSCSIDevice::MonitorScreen);
	monitor->SetSize(MONITOR_WIDTH, 3 + vqlines);
}

// デストラクタ
VirtIOSCSIDevice::~VirtIOSCSIDevice()
{
}

// 動的なコンストラクション
bool
VirtIOSCSIDevice::Create()
{
	if (inherited::Create() == false) {
		return false;
	}

	try {
		domain.reset(new SCSIDomain(this, "VIOSCSI", "virtio-scsi"));
	} catch (...) { }
	if ((bool)domain == false) {
		warnx("Failed to initialize SCSIDomain at %s", __method__);
		return false;
	}

	return true;
}

bool
VirtIOSCSIDevice::Init()
{
	if (inherited::Init() == false) {
		return false;
	}

	host = domain->GetInitiator();

	return true;
}

void
VirtIOSCSIDevice::ResetHard(bool poweron)
{
	// sense_size, cdb_size はリセットで元に戻る (5.6.4)。
	// XXX 書き換えに対応?
	sense_size = 96;
	cdb_size = 32;

	// DEVICE_FEATURES と構成レイアウトを用意。
	VirtIOSCSIConfigWriter cfg;
	cfg.num_queues = 1;
	cfg.seg_max = 6;	// XXX ?
	cfg.max_sectors = 256;
	cfg.cmd_per_lun = 1;
	cfg.sense_size = sense_size;
	cfg.cdb_size = cdb_size;
	cfg.max_channel = 0;
	cfg.max_target = 7;
	cfg.max_lun = 0;
	cfg.WriteTo(&device_config[0]);
}

void
VirtIOSCSIDevice::MonitorScreen(Monitor *, TextScreen& screen)
{
	int y = 0;

	screen.Clear();

	y = MonitorScreenDev(screen, y);

	y++;
	for (const auto& q : vqueues) {
		y = MonitorScreenVirtQueue(screen, y, q);
		y++;
		y = MonitorScreenVirtQDesc(screen, y, q);
		y++;
	}
}

// ディスクリプタを一つ処理する。
void
VirtIOSCSIDevice::ProcessDesc(VirtIOReq& req)
{
	VirtQueue *q = req.q;

	switch (q->idx) {
	 case 0:
		ProcessDescControl(req);
		break;
	 case 1:
		ProcessDescEvent(req);
		break;
	 case 2:
		ProcessDescRequest(req);
		break;
	 default:
		assert(q->idx < 3);
	}
}

// Control キューのディスクリプタを一つ処理する。
void
VirtIOSCSIDevice::ProcessDescControl(VirtIOReq& req)
{
	putlog(0, "Control Queue (NOT IMPLEMENTED)");
}

// Event キューのディスクリプタを一つ処理する。
void
VirtIOSCSIDevice::ProcessDescEvent(VirtIOReq& req)
{
	putlog(0, "Event Queue (NOT IMPLEMENTED)");
}

// Request キューのディスクリプタを一つ処理する。
void
VirtIOSCSIDevice::ProcessDescRequest(VirtIOReq& req)
{
	// 8バイトの lun はこういう構成。
	// bus は 1 から数え始める。
	// ID が SCSI ID に相当するところ (0-255)。
	// LUN は 0-16387。0x4000 が立っているのは何?
	// 下位 32 ビットは論理ユニット固有のアドレスフィールド (0)。
	// 複数バイトのフィールドは BE 順にメモリに置かれている。
	//
	//   +0    +1    +2    +3    +4    +5    +6    +7
	// +-----+-----+-----+-----+-----+-----+-----+-----+
	// | bus |  ID |    LUN    |           0           |
	// +-----+-----+-----+-----+-----+-----+-----+-----+
	uint32 bus  = ReqReadU8(req);
	uint32 id   = ReqReadU8(req);
	uint32 lun  = ReqReadBE16(req);
	uint32 addr = ReqReadBE32(req);
	putlog(2, "Bus=$%02x ID=$%02x LUN=$%04x Addr=$%08x", bus, id, lun, addr);

	// 仕様ではこっちが id という名前だが紛らわしいし使わないので tag にする。
	// QoS Tag みたいなもの?
	uint64 tag;
	ReqReadLE64(req, &tag);
	putlog(3, "tagID=%08x'%08x", (uint32)(tag >> 32), (uint32)tag);

	uint32 task_attr = ReqReadU8(req);
	uint32 prio = ReqReadU8(req);
	uint32 crn = ReqReadU8(req);
	putlog(3, "task_attr=%02x prio=%02x crn=%02x",
		task_attr, prio, crn);

	// コマンド(CDB) を取り出す。
	std::vector<uint8> cdb(cdb_size);
	for (int i = 0; i < cdb_size; i++) {
		cdb[i] = ReqReadU8(req);
	}
	if (loglevel >= 2
#if defined(NO_READWRITE_LOG)
		&& (cdb[0] != SCSI::Command::Read6 &&
		    cdb[0] != SCSI::Command::Read10 &&
		    cdb[0] != SCSI::Command::Write6 &&
		    cdb[0] != SCSI::Command::Write10)
#endif
	) {
		std::string cmdbuf;
		const char *name = SCSI::GetCommandName(cdb[0]);
		cmdbuf += string_format("Command \"%s\"", name ?: "?");
		for (const auto& v : cdb) {
			cmdbuf += string_format(" %02x", v);
		}
		putlogn("%s", cmdbuf.c_str());
	}

	// 読み込み側は、ここから後ろがあれば dataout[]。
	uint32 dataout_len = req.rremain();
	// datain[] も計算。12 は .sense_len から .response までのバイト数。
	uint32 datain_len = req.wlen - 12 - sense_size;

	// ターゲット ID が存在するか。
	auto *target = domain->GetTarget(id);
	if (target == NULL) {
		ProcessDescRequestError(req, VIRTIO_SCSI_S_BAD_TARGET,
			dataout_len + datain_len);
		return;
	}

	// 両方向データ (F_INOUT) はサポートしていない
	if (dataout_len > 0 && datain_len > 0) {
		ProcessDescRequestError(req, VIRTIO_SCSI_S_FAILURE,
			dataout_len + datain_len);
		return;
	}

	// ID と読み書き方向が分かったので、アクセスインジケータ用に記録。
	// 少なくとも dataout[] があれば書き込み、それ以外を読み込みとする。
	if (dataout_len > 0) {
		access[id * 2 + 1] = req.q->last_avail_idx;
	} else {
		access[id * 2 + 0] = req.q->last_avail_idx;
	}

	// ターゲットでコマンドを起動してみる。
	SCSICmd *cmd = target->SelectCommand(cdb);
	if (cmd == NULL) {
		// インスタンスの生成に失敗した (未実装コマンドではない)
		ProcessDescRequestError(req, VIRTIO_SCSI_S_FAILURE,
			dataout_len + datain_len);
		return;
	}

	std::vector<uint8> databuf;
	std::vector<uint8> statusbuf;
	std::vector<uint8> msgbuf;
	// 初手は必ずコマンドフェース。
	// コマンドフェーズの次からは動的に遷移。
	auto phase = cmd->ExecCommand(cdb);

	for (; phase != SCSI::XferPhase::End; ) {
		SCSI::XferPhase next;
		switch (phase) {
		 case SCSI::XferPhase::DataOut:	// イニシエータ → ターゲット
			cmd->buf.clear();
			cmd->buf.resize(req.rremain());
			ReqReadRegion(req, cmd->buf.data(), cmd->buf.size());
			putlog(3, "DataOut begin (%zu bytes)", cmd->buf.size());
			next = cmd->DoneDataOut();
			break;

		 case SCSI::XferPhase::DataIn:	// イニシエータ ← ターゲット
			next = cmd->DoneDataIn();
			databuf = cmd->buf;
			putlog(3, "DataIn begin (%u bytes)", (uint)databuf.size());
			break;

		 case SCSI::XferPhase::MsgOut:
			cmd->buf.clear();
			cmd->BeginMsgOut();
			next = cmd->DoneMsgOut();
			putlog(3, "MsgOut begin (%u bytes)", (uint)cmd->buf.size());
			break;

		 case SCSI::XferPhase::MsgIn:
			cmd->BeginMsgIn();
			next = cmd->DoneMsgIn();
			msgbuf = cmd->buf;
			putlog(3, "MsgIn begin (%u bytes)", (uint)msgbuf.size());
			break;

		 case SCSI::XferPhase::Status:
			cmd->BeginStatus();
			next = cmd->DoneStatus();
			statusbuf = cmd->buf;
			putlog(3, "Status begin (%u bytes)", (uint)statusbuf.size());
			break;

		 default:
			PANIC("%s unknown phase done %d", __func__, (int)phase);
		}
		putlog(3, "%s done", SCSI::GetXferPhaseName(phase));
		phase = next;
	}

	// dataout が残っていれば読み飛ばす?
	req.rskip();

	// sense バッファを作成。
	// 内容は ScSICmdRequestSense::ExecCommand() のコード参照。
	uint32 sense_len = 18;
	std::vector<uint8> sensebuf(sense_len);
	uint32 sensekey = cmd->GetTarget()->sensekey;
	uint32 senseasc = cmd->GetTarget()->senseasc;
	sensebuf[0]  = 0x70;	// Valid(?)
	sensebuf[2]  = sensekey;
	sensebuf[12] = senseasc >> 8;
	sensebuf[13] = senseasc & 0xff;
	if (sense_size < sense_len) {
		sense_len = sense_size;
	}
	sensebuf.resize(sense_size);

	// residual は未処理の"データ"バイト数?
	uint32 residual = 0;	// XXX よく分からん
	ReqWriteLE32(req, sense_len);
	ReqWriteLE32(req, residual);
	ReqWriteLE16(req, 0);	// status_qualifier
	if (statusbuf.size() == 0) {
		// ステータスフェーズが来なかった?
		ReqWriteU8(req, 0);
		ReqWriteU8(req, VIRTIO_SCSI_S_FAILURE);
		return;
	}

	ReqWriteU8(req, statusbuf[0]);
	ReqWriteU8(req, VIRTIO_SCSI_S_OK);
	// sense[sense_size] は常に sense_size バイト分。
	ReqWriteRegion(req, sensebuf.data(), sensebuf.size());

	// datain ならコマンドが作成したバッファをメモリに書き出す。
	if (databuf.empty() == false) {
		ReqWriteRegion(req, databuf.data(), databuf.size());
	}
}

// Request キューのエラー応答を作成する。
// wpos は書き込みセグメントの先頭を指していること。
void
VirtIOSCSIDevice::ProcessDescRequestError(VirtIOReq& req,
	uint32 response_, uint32 residual_)
{
	// よく分からんけど readable part は読み捨てておく?
	req.rskip();

	// writable part の先頭にいるはず。
	assert(req.wpos() == 0);
	ReqWriteLE32(req, 0);	// sense_len
	ReqWriteLE32(req, residual_);
	ReqWriteLE16(req, 0);	// status_qualifier
	ReqWriteU8(req, 0);		// status
	ReqWriteU8(req, response_);
}

const char *
VirtIOSCSIDevice::GetFeatureName(uint feature) const
{
	static std::pair<uint, const char *> names[] = {
		{ VIRTIO_SCSI_F_INOUT,			"INOUT" },
		{ VIRTIO_SCSI_F_HOTPLUG,		"HOTPLUG" },
		{ VIRTIO_SCSI_F_CHANGE,			"CHANGE" },
		{ VIRTIO_SCSI_F_T10_PI,			"T10_PI" },
	};

	for (auto& p : names) {
		if (feature == p.first) {
			return p.second;
		}
	}
	return inherited::GetFeatureName(feature);
}

// デバイス構成レイアウトを書き出す。
void
VirtIOSCSIConfigWriter::WriteTo(uint8 *dst) const
{
	MemoryStreamLE mem(dst);

	mem.Write4(num_queues);
	mem.Write4(seg_max);
	mem.Write4(max_sectors);
	mem.Write4(cmd_per_lun);
	mem.Write4(event_info_size);
	mem.Write4(sense_size);
	mem.Write4(cdb_size);
	mem.Write2(max_channel);
	mem.Write2(max_target);
	mem.Write4(max_lun);
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.