declare let DEBUG: boolean import { LOG_VIRTIO } from './const.js' import { h, int_log2 } from './lib.js' import { dbg_assert, dbg_log } from './log.js' // Minimal interface for the CPU fields VirtIO needs interface VirtioCPU { io: VirtioIO devices: { pci: VirtioPCI net?: unknown } read16(addr: number): number read32s(addr: number): number write16(addr: number, value: number): void write32(addr: number, value: number): void read_blob(addr: number, length: number): Uint8Array write_blob(blob: Uint8Array, addr: number): void zero_memory(addr: number, length: number): void memory_size: Int32Array } interface VirtioIO { register_read( port: number, device: object, r8?: ((port: number) => number) | undefined, r16?: ((port: number) => number) | undefined, r32?: ((port: number) => number) | undefined, ): void register_write( port: number, device: object, w8?: ((port: number) => void) | undefined, w16?: ((port: number) => void) | undefined, w32?: ((port: number) => void) | undefined, ): void } interface VirtioPCI { register_device(device: VirtIO): void raise_irq(pci_id: number): void lower_irq(pci_id: number): void } // http://docs.oasis-open.org/virtio/virtio/v1.0/virtio-v1.0.html const VIRTIO_PCI_VENDOR_ID = 0x1af4 // Identifies vendor-specific PCI capability. const VIRTIO_PCI_CAP_VENDOR = 0x09 // Length (bytes) of VIRTIO_PCI_CAP linked list entry. const VIRTIO_PCI_CAP_LENGTH = 16 // Capability types. const VIRTIO_PCI_CAP_COMMON_CFG = 1 const VIRTIO_PCI_CAP_NOTIFY_CFG = 2 const VIRTIO_PCI_CAP_ISR_CFG = 3 const VIRTIO_PCI_CAP_DEVICE_CFG = 4 const VIRTIO_PCI_CAP_PCI_CFG = 5 // Status bits (device_status values). const VIRTIO_STATUS_ACKNOWLEDGE = 1 const VIRTIO_STATUS_DRIVER = 2 const VIRTIO_STATUS_DRIVER_OK = 4 const VIRTIO_STATUS_FEATURES_OK = 8 const VIRTIO_STATUS_DEVICE_NEEDS_RESET = 64 const VIRTIO_STATUS_FAILED = 128 // ISR bits (isr_status values). const VIRTIO_ISR_QUEUE = 1 const VIRTIO_ISR_DEVICE_CFG = 2 // Feature bits (bit positions). export const VIRTIO_F_RING_INDIRECT_DESC = 28 export const VIRTIO_F_RING_EVENT_IDX = 29 export const VIRTIO_F_VERSION_1 = 32 // Queue struct sizes. // Size (bytes) of the virtq_desc struct per queue size. const VIRTQ_DESC_ENTRYSIZE = 16 // Size (bytes) of the virtq_avail struct ignoring ring entries. const _VIRTQ_AVAIL_BASESIZE = 6 // Size (bytes) of the virtq_avail struct per queue size. const VIRTQ_AVAIL_ENTRYSIZE = 2 // Size (bytes) of the virtq_used struct ignoring ring entries. const _VIRTQ_USED_BASESIZE = 6 // Size (bytes) of the virtq_desc struct per queue size. const VIRTQ_USED_ENTRYSIZE = 8 // Mask for wrapping the idx field of the virtq_used struct so that the value // naturally overflows after 65535 (idx is a word). const VIRTQ_IDX_MASK = 0xffff // Queue flags. const VIRTQ_DESC_F_NEXT = 1 const VIRTQ_DESC_F_WRITE = 2 const VIRTQ_DESC_F_INDIRECT = 4 const VIRTQ_AVAIL_F_NO_INTERRUPT = 1 const _VIRTQ_USED_F_NO_NOTIFY = 1 // TypeScript types. export interface VirtIO_CapabilityStructEntry { bytes: number name: string read: () => number write: (data: number) => void } export type VirtIO_CapabilityStruct = VirtIO_CapabilityStructEntry[] export interface VirtIO_CapabilityInfo { type: number bar: number port: number use_mmio: boolean offset: number extra: Uint8Array struct: VirtIO_CapabilityStruct } export interface VirtQueue_Options { size_supported: number notify_offset: number } export interface VirtIO_CommonCapabilityOptions { initial_port: number queues: VirtQueue_Options[] features: number[] on_driver_ok: () => void } export interface VirtIO_NotificationCapabilityOptions { initial_port: number single_handler: boolean handlers: ((queue_id: number) => void)[] } export interface VirtIO_ISRCapabilityOptions { initial_port: number } export interface VirtIO_DeviceSpecificCapabilityOptions { initial_port: number struct: VirtIO_CapabilityStruct } export interface VirtIO_Options { name: string pci_id: number device_id: number subsystem_device_id: number common: VirtIO_CommonCapabilityOptions notification: VirtIO_NotificationCapabilityOptions isr_status: VirtIO_ISRCapabilityOptions device_specific?: VirtIO_DeviceSpecificCapabilityOptions } interface VirtQueueDescriptor { addr_low: number addr_high: number len: number flags: number next: number } export class VirtIO { cpu: VirtioCPU pci: VirtioPCI device_id: number pci_space: number[] pci_id: number pci_bars: { size: number }[] name: string device_feature_select: number driver_feature_select: number device_feature: Uint32Array driver_feature: Uint32Array features_ok: boolean device_status: number config_has_changed: boolean config_generation: number queues: VirtQueue[] queue_select: number queue_selected: VirtQueue | null isr_status: number constructor(cpu: VirtioCPU, options: VirtIO_Options) { this.cpu = cpu this.pci = cpu.devices.pci this.device_id = options.device_id this.pci_space = [ // Vendor ID VIRTIO_PCI_VENDOR_ID & 0xff, VIRTIO_PCI_VENDOR_ID >> 8, // Device ID options.device_id & 0xff, options.device_id >> 8, // Command 0x07, 0x05, // Status - enable capabilities list 0x10, 0x00, // Revision ID 0x01, // Prof IF, Subclass, Class code 0x00, 0x02, 0x00, // Cache line size 0x00, // Latency Timer 0x00, // Header Type 0x00, // Built-in self test 0x00, // BAR0 0x01, 0xa8, 0x00, 0x00, // BAR1 0x00, 0x10, 0xbf, 0xfe, // BAR2 0x00, 0x00, 0x00, 0x00, // BAR3 0x00, 0x00, 0x00, 0x00, // BAR4 0x00, 0x00, 0x00, 0x00, // BAR5 0x00, 0x00, 0x00, 0x00, // CardBus CIS pointer 0x00, 0x00, 0x00, 0x00, // Subsystem vendor ID VIRTIO_PCI_VENDOR_ID & 0xff, VIRTIO_PCI_VENDOR_ID >> 8, // Subsystem ID options.subsystem_device_id & 0xff, options.subsystem_device_id >> 8, // Expansion ROM base address 0x00, 0x00, 0x00, 0x00, // Capabilities pointer 0x40, // Reserved 0x00, 0x00, 0x00, // Reserved 0x00, 0x00, 0x00, 0x00, // Interrupt line 0x00, // Interrupt pin 0x01, // Min grant 0x00, // Max latency 0x00, ] // Prevent sparse arrays by preallocating. this.pci_space = this.pci_space.concat( Array(256 - this.pci_space.length).fill(0), ) // Remaining PCI space is appended by capabilities further below. this.pci_id = options.pci_id // PCI bars gets filled in by capabilities further below. this.pci_bars = [] this.name = options.name // Feature bits grouped in dwords, dword selected by device_feature_select. this.device_feature_select = 0 this.driver_feature_select = 0 // Unspecified upper bound. Assume 4*32=128 bits. this.device_feature = new Uint32Array(4) this.driver_feature = new Uint32Array(4) for (const f of options.common.features) { dbg_assert( f >= 0, 'VirtIO device<' + this.name + '> feature bit numbers must be non-negative', ) dbg_assert( f < 128, 'VirtIO device<' + this.name + '> feature bit numbers assumed less than 128 in implementation', ) // Feature bits are grouped in 32 bits. this.device_feature[f >>> 5] |= 1 << (f & 0x1f) this.driver_feature[f >>> 5] |= 1 << (f & 0x1f) } dbg_assert( options.common.features.includes(VIRTIO_F_VERSION_1), 'VirtIO device<' + this.name + '> only non-transitional devices are supported', ) // Indicates whether driver_feature bits is subset of device_feature bits. this.features_ok = true this.device_status = 0 this.config_has_changed = false this.config_generation = 0 this.queues = [] for (const queue_options of options.common.queues) { this.queues.push(new VirtQueue(cpu, this, queue_options)) } this.queue_select = 0 this.queue_selected = this.queues[0] this.isr_status = 0 // Verify notification options. if (DEBUG) { const offsets = new Set() for (const offset of this.queues.map((q) => q.notify_offset)) { const effective_offset = options.notification.single_handler ? 0 : offset offsets.add(effective_offset) dbg_assert( !!options.notification.handlers[effective_offset], 'VirtIO device<' + this.name + "> every queue's notifier must exist", ) } for (const [ index, handler, ] of options.notification.handlers.entries()) { dbg_assert( !handler || offsets.has(index), 'VirtIO device<' + this.name + '> no defined notify handler should be unused', ) } } const capabilities: VirtIO_CapabilityInfo[] = [] capabilities.push(this.create_common_capability(options.common)) capabilities.push( this.create_notification_capability(options.notification), ) capabilities.push(this.create_isr_capability(options.isr_status)) if (options.device_specific) { capabilities.push( this.create_device_specific_capability(options.device_specific), ) } this.init_capabilities(capabilities) cpu.devices.pci.register_device(this) this.reset() } create_common_capability( options: VirtIO_CommonCapabilityOptions, ): VirtIO_CapabilityInfo { return { type: VIRTIO_PCI_CAP_COMMON_CFG, bar: 0, port: options.initial_port, use_mmio: false, offset: 0, extra: new Uint8Array(0), struct: [ { bytes: 4, name: 'device_feature_select', read: () => this.device_feature_select, write: (data: number) => { this.device_feature_select = data }, }, { bytes: 4, name: 'device_feature', read: () => this.device_feature[this.device_feature_select] || 0, write: (_data: number) => { /* read only */ }, }, { bytes: 4, name: 'driver_feature_select', read: () => this.driver_feature_select, write: (data: number) => { this.driver_feature_select = data }, }, { bytes: 4, name: 'driver_feature', read: () => this.driver_feature[this.driver_feature_select] || 0, write: (data: number) => { const supported_feature = this.device_feature[this.driver_feature_select] if ( this.driver_feature_select < this.driver_feature.length ) { // Note: only set subset of device_features is set. // Required in our implementation for is_feature_negotiated(). this.driver_feature[this.driver_feature_select] = data & supported_feature } // Check that driver features is an inclusive subset of device features. const invalid_bits = data & ~supported_feature this.features_ok = this.features_ok && !invalid_bits }, }, { bytes: 2, name: 'msix_config', read: () => { dbg_log('No msi-x capability supported.', LOG_VIRTIO) return 0xffff }, write: (_data: number) => { dbg_log('No msi-x capability supported.', LOG_VIRTIO) }, }, { bytes: 2, name: 'num_queues', read: () => this.queues.length, write: (_data: number) => { /* read only */ }, }, { bytes: 1, name: 'device_status', read: () => this.device_status, write: (data: number) => { if (data === 0) { dbg_log( 'Reset device<' + this.name + '>', LOG_VIRTIO, ) this.reset() } else if (data & VIRTIO_STATUS_FAILED) { dbg_log( 'Warning: Device<' + this.name + '> status failed', LOG_VIRTIO, ) } else { dbg_log( 'Device<' + this.name + '> status: ' + (data & VIRTIO_STATUS_ACKNOWLEDGE ? 'ACKNOWLEDGE ' : '') + (data & VIRTIO_STATUS_DRIVER ? 'DRIVER ' : '') + (data & VIRTIO_STATUS_DRIVER_OK ? 'DRIVER_OK' : '') + (data & VIRTIO_STATUS_FEATURES_OK ? 'FEATURES_OK ' : '') + (data & VIRTIO_STATUS_DEVICE_NEEDS_RESET ? 'DEVICE_NEEDS_RESET' : ''), LOG_VIRTIO, ) } // Don't set FEATURES_OK if our device doesn't support requested features. if (!this.features_ok) { if (DEBUG && data & VIRTIO_STATUS_FEATURES_OK) { dbg_log('Removing FEATURES_OK', LOG_VIRTIO) } data &= ~VIRTIO_STATUS_FEATURES_OK } const prev_status = this.device_status this.device_status = data if (data & ~prev_status & VIRTIO_STATUS_DRIVER_OK) { if ( prev_status & VIRTIO_STATUS_DEVICE_NEEDS_RESET ) { // We couldn't notify NEEDS_RESET earlier because DRIVER_OK was not set. // Now it has been set, notify now. this.notify_config_changes() } options.on_driver_ok() } }, }, { bytes: 1, name: 'config_generation', read: () => this.config_generation, write: (_data: number) => { /* read only */ }, }, { bytes: 2, name: 'queue_select', read: () => this.queue_select, write: (data: number) => { this.queue_select = data if (this.queue_select < this.queues.length) { this.queue_selected = this.queues[this.queue_select] } else { // Allow queue_select >= num_queues. this.queue_selected = null // Drivers can then detect that the queue is not available // using the below fields. } }, }, { bytes: 2, name: 'queue_size', read: () => this.queue_selected ? this.queue_selected.size : 0, write: (data: number) => { if (!this.queue_selected) { return } if (data & (data - 1)) { dbg_log( 'Warning: dev<' + this.name + '> ' + 'Given queue size was not a power of 2. ' + 'Rounding up to next power of 2.', LOG_VIRTIO, ) data = 1 << (int_log2(data - 1) + 1) } if (data > this.queue_selected.size_supported) { dbg_log( 'Warning: dev<' + this.name + '> ' + 'Trying to set queue size greater than supported. ' + 'Clamping to supported size.', LOG_VIRTIO, ) data = this.queue_selected.size_supported } this.queue_selected.set_size(data) }, }, { bytes: 2, name: 'queue_msix_vector', read: () => { dbg_log('No msi-x capability supported.', LOG_VIRTIO) return 0xffff }, write: (_data: number) => { dbg_log('No msi-x capability supported.', LOG_VIRTIO) }, }, { bytes: 2, name: 'queue_enable', read: () => this.queue_selected ? this.queue_selected.enabled ? 1 : 0 : 0, write: (data: number) => { if (!this.queue_selected) { return } if (data === 1) { if (this.queue_selected.is_configured()) { this.queue_selected.enable() } else { dbg_log( 'Driver bug: tried enabling unconfigured queue', LOG_VIRTIO, ) } } else if (data === 0) { dbg_log( 'Driver bug: tried writing 0 to queue_enable', LOG_VIRTIO, ) } }, }, { bytes: 2, name: 'queue_notify_off', read: () => this.queue_selected ? this.queue_selected.notify_offset : 0, write: (_data: number) => { /* read only */ }, }, { bytes: 4, name: 'queue_desc (low dword)', read: () => this.queue_selected ? this.queue_selected.desc_addr : 0, write: (data: number) => { if (this.queue_selected) this.queue_selected.desc_addr = data }, }, { bytes: 4, name: 'queue_desc (high dword)', read: () => 0, write: (data: number) => { if (data !== 0) dbg_log( 'Warning: High dword of 64 bit queue_desc ignored:' + data, LOG_VIRTIO, ) }, }, { bytes: 4, name: 'queue_avail (low dword)', read: () => this.queue_selected ? this.queue_selected.avail_addr : 0, write: (data: number) => { if (this.queue_selected) this.queue_selected.avail_addr = data }, }, { bytes: 4, name: 'queue_avail (high dword)', read: () => 0, write: (data: number) => { if (data !== 0) dbg_log( 'Warning: High dword of 64 bit queue_avail ignored:' + data, LOG_VIRTIO, ) }, }, { bytes: 4, name: 'queue_used (low dword)', read: () => this.queue_selected ? this.queue_selected.used_addr : 0, write: (data: number) => { if (this.queue_selected) this.queue_selected.used_addr = data }, }, { bytes: 4, name: 'queue_used (high dword)', read: () => 0, write: (data: number) => { if (data !== 0) dbg_log( 'Warning: High dword of 64 bit queue_used ignored:' + data, LOG_VIRTIO, ) }, }, ], } } create_notification_capability( options: VirtIO_NotificationCapabilityOptions, ): VirtIO_CapabilityInfo { const notify_struct: VirtIO_CapabilityStruct = [] let notify_off_multiplier: number if (options.single_handler) { dbg_assert( options.handlers.length === 1, 'VirtIO device<' + this.name + '> too many notify handlers specified: expected single handler', ) // Forces all queues to use the same address for notifying. notify_off_multiplier = 0 } else { notify_off_multiplier = 2 } for (const [i, handler] of options.handlers.entries()) { notify_struct.push({ bytes: 2, name: 'notify' + i, read: () => 0xffff, write: handler || ((_data: number) => {}), }) } return { type: VIRTIO_PCI_CAP_NOTIFY_CFG, bar: 1, port: options.initial_port, use_mmio: false, offset: 0, extra: new Uint8Array([ notify_off_multiplier & 0xff, (notify_off_multiplier >> 8) & 0xff, (notify_off_multiplier >> 16) & 0xff, notify_off_multiplier >> 24, ]), struct: notify_struct, } } create_isr_capability( options: VirtIO_ISRCapabilityOptions, ): VirtIO_CapabilityInfo { return { type: VIRTIO_PCI_CAP_ISR_CFG, bar: 2, port: options.initial_port, use_mmio: false, offset: 0, extra: new Uint8Array(0), struct: [ { bytes: 1, name: 'isr_status', read: () => { const isr_status = this.isr_status this.lower_irq() return isr_status }, write: (_data: number) => { /* read only */ }, }, ], } } create_device_specific_capability( options: VirtIO_DeviceSpecificCapabilityOptions, ): VirtIO_CapabilityInfo { dbg_assert( !(options.initial_port & 0x3), 'VirtIO device<' + this.name + '> device specific cap offset must be 4-byte aligned', ) return { type: VIRTIO_PCI_CAP_DEVICE_CFG, bar: 3, port: options.initial_port, use_mmio: false, offset: 0, extra: new Uint8Array(0), struct: options.struct, } } // Writes capabilities into pci_space and hook up IO/MMIO handlers. // Call only within constructor. init_capabilities(capabilities: VirtIO_CapabilityInfo[]): void { // Next available offset for capabilities linked list. let cap_next = (this.pci_space[0x34] = 0x40) // Current offset. let cap_ptr for (const cap of capabilities) { const cap_len = VIRTIO_PCI_CAP_LENGTH + cap.extra.length cap_ptr = cap_next cap_next = cap_ptr + cap_len dbg_assert( cap_next <= 256, 'VirtIO device<' + this.name + "> can't fit all capabilities into 256byte configspace", ) dbg_assert( 0 <= cap.bar && cap.bar < 6, 'VirtIO device<' + this.name + '> capability invalid bar number', ) let bar_size = cap.struct.reduce( (bytes, field) => bytes + field.bytes, 0, ) bar_size += cap.offset // Round up to next power of 2, // Minimum 16 bytes for its size to be detectable in general (esp. mmio). bar_size = bar_size < 16 ? 16 : 1 << (int_log2(bar_size - 1) + 1) dbg_assert( (cap.port & (bar_size - 1)) === 0, 'VirtIO device<' + this.name + '> capability port should be aligned to pci bar size', ) this.pci_bars[cap.bar] = { size: bar_size, } this.pci_space[cap_ptr] = VIRTIO_PCI_CAP_VENDOR this.pci_space[cap_ptr + 1] = cap_next this.pci_space[cap_ptr + 2] = cap_len this.pci_space[cap_ptr + 3] = cap.type this.pci_space[cap_ptr + 4] = cap.bar this.pci_space[cap_ptr + 5] = 0 // Padding. this.pci_space[cap_ptr + 6] = 0 // Padding. this.pci_space[cap_ptr + 7] = 0 // Padding. this.pci_space[cap_ptr + 8] = cap.offset & 0xff this.pci_space[cap_ptr + 9] = (cap.offset >>> 8) & 0xff this.pci_space[cap_ptr + 10] = (cap.offset >>> 16) & 0xff this.pci_space[cap_ptr + 11] = cap.offset >>> 24 this.pci_space[cap_ptr + 12] = bar_size & 0xff this.pci_space[cap_ptr + 13] = (bar_size >>> 8) & 0xff this.pci_space[cap_ptr + 14] = (bar_size >>> 16) & 0xff this.pci_space[cap_ptr + 15] = bar_size >>> 24 for (const [i, extra_byte] of cap.extra.entries()) { this.pci_space[cap_ptr + 16 + i] = extra_byte } const bar_offset = 0x10 + 4 * cap.bar this.pci_space[bar_offset] = (cap.port & 0xfe) | +!cap.use_mmio this.pci_space[bar_offset + 1] = (cap.port >>> 8) & 0xff this.pci_space[bar_offset + 2] = (cap.port >>> 16) & 0xff this.pci_space[bar_offset + 3] = (cap.port >>> 24) & 0xff let port = cap.port + cap.offset for (const field of cap.struct) { let read: (...args: any[]) => number = field.read let write: (...args: any[]) => void = field.write if (DEBUG) { read = () => { const val = field.read() dbg_log( 'Device<' + this.name + '> ' + 'cap[' + cap.type + '] ' + 'read[' + field.name + '] ' + '=> ' + h(val, field.bytes * 8), LOG_VIRTIO, ) return val } write = (data: number) => { dbg_log( 'Device<' + this.name + '> ' + 'cap[' + cap.type + '] ' + 'write[' + field.name + '] ' + '<= ' + h(data, field.bytes * 8), LOG_VIRTIO, ) field.write(data) } } if (cap.use_mmio) { dbg_assert( false, 'VirtIO device <' + this.name + '> mmio capability not implemented.', ) } else { // DSL (2.4 kernel) does these reads const shim_read8_on_16 = function (addr: number): number { dbg_log( 'Warning: 8-bit read from 16-bit virtio port', LOG_VIRTIO, ) return (read(addr & ~1) >> ((addr & 1) << 3)) & 0xff } const shim_read8_on_32 = function (addr: number): number { dbg_log( 'Warning: 8-bit read from 32-bit virtio port', LOG_VIRTIO, ) return (read(addr & ~3) >> ((addr & 3) << 3)) & 0xff } // archhurd does these reads const shim_read32_on_16 = function (addr: number): number { dbg_log( 'Warning: 32-bit read from 16-bit virtio port', LOG_VIRTIO, ) return read(addr) } switch (field.bytes) { case 4: this.cpu.io.register_read( port, this, shim_read8_on_32, undefined, read, ) this.cpu.io.register_read( port + 1, this, shim_read8_on_32, ) this.cpu.io.register_read( port + 2, this, shim_read8_on_32, ) this.cpu.io.register_read( port + 3, this, shim_read8_on_32, ) this.cpu.io.register_write( port, this, undefined, undefined, write, ) break case 2: this.cpu.io.register_read( port, this, shim_read8_on_16, read, shim_read32_on_16, ) this.cpu.io.register_read( port + 1, this, shim_read8_on_16, ) this.cpu.io.register_write( port, this, undefined, write, ) break case 1: this.cpu.io.register_read(port, this, read) this.cpu.io.register_write(port, this, write) break default: dbg_assert( false, 'VirtIO device <' + this.name + '> invalid capability field width of ' + field.bytes + ' bytes', ) break } } port += field.bytes } } // Terminate linked list with the pci config access capability. const cap_len = VIRTIO_PCI_CAP_LENGTH + 4 dbg_assert( cap_next + cap_len <= 256, 'VirtIO device<' + this.name + "> can't fit all capabilities into 256byte configspace", ) this.pci_space[cap_next] = VIRTIO_PCI_CAP_VENDOR this.pci_space[cap_next + 1] = 0 // cap next (null terminator) this.pci_space[cap_next + 2] = cap_len this.pci_space[cap_next + 3] = VIRTIO_PCI_CAP_PCI_CFG // cap type this.pci_space[cap_next + 4] = 0 // bar (written by device) this.pci_space[cap_next + 5] = 0 // Padding. this.pci_space[cap_next + 6] = 0 // Padding. this.pci_space[cap_next + 7] = 0 // Padding. // Remaining fields are configured by driver when needed. // offset this.pci_space[cap_next + 8] = 0 this.pci_space[cap_next + 9] = 0 this.pci_space[cap_next + 10] = 0 this.pci_space[cap_next + 11] = 0 // bar size this.pci_space[cap_next + 12] = 0 this.pci_space[cap_next + 13] = 0 this.pci_space[cap_next + 14] = 0 this.pci_space[cap_next + 15] = 0 // cfg_data this.pci_space[cap_next + 16] = 0 this.pci_space[cap_next + 17] = 0 this.pci_space[cap_next + 18] = 0 this.pci_space[cap_next + 19] = 0 // // TODO // The pci config access capability is required by spec, but so far, devices // seem to work well without it. // This capability provides a cfg_data field (at cap_next + 16 for 4 bytes) // that acts like a window to the previous bars. The driver writes the bar number, // offset, and length values in this capability, and the cfg_data field should // mirror the data referred by the bar, offset and length. Here, length can be // 1, 2, or 4. // // This requires some sort of pci devicespace read and write handlers. } get_state(): any[] { let state: any[] = [] state[0] = this.device_feature_select state[1] = this.driver_feature_select state[2] = this.device_feature state[3] = this.driver_feature state[4] = this.features_ok state[5] = this.device_status state[6] = this.config_has_changed state[7] = this.config_generation state[8] = this.isr_status state[9] = this.queue_select state = state.concat(this.queues) return state } set_state(state: any[]): void { this.device_feature_select = state[0] this.driver_feature_select = state[1] this.device_feature = state[2] this.driver_feature = state[3] this.features_ok = state[4] this.device_status = state[5] this.config_has_changed = state[6] this.config_generation = state[7] this.isr_status = state[8] this.queue_select = state[9] let i = 0 for (const queue of state.slice(10)) { this.queues[i].set_state(queue) i++ } this.queue_selected = this.queues[this.queue_select] || null } reset(): void { this.device_feature_select = 0 this.driver_feature_select = 0 this.driver_feature.set(this.device_feature) this.features_ok = true this.device_status = 0 this.queue_select = 0 this.queue_selected = this.queues[0] for (const queue of this.queues) { queue.reset() } this.config_has_changed = false this.config_generation = 0 this.lower_irq() } // Call this when device-specific configuration state changes. // Also called when status DEVICE_NEEDS_RESET is set. notify_config_changes(): void { this.config_has_changed = true if (this.device_status & VIRTIO_STATUS_DRIVER_OK) { this.raise_irq(VIRTIO_ISR_DEVICE_CFG) } else { dbg_assert( false, 'VirtIO device<' + this.name + '> attempted to notify driver before DRIVER_OK', ) } } // To be called after reading any field whose write can trigger notify_config_changes(). update_config_generation(): void { if (this.config_has_changed) { this.config_generation++ this.config_generation &= 0xff this.config_has_changed = false } } is_feature_negotiated(feature: number): boolean { // Feature bits are grouped in 32 bits. // Note: earlier we chose not to set invalid features into driver_feature. return ( (this.driver_feature[feature >>> 5] & (1 << (feature & 0x1f))) > 0 ) } // Call this if an irrecoverable error has occurred. // Notifies driver if DRIVER_OK, or when DRIVER_OK gets set. needs_reset(): void { dbg_log( 'Device<' + this.name + '> experienced error - requires reset', LOG_VIRTIO, ) this.device_status |= VIRTIO_STATUS_DEVICE_NEEDS_RESET if (this.device_status & VIRTIO_STATUS_DRIVER_OK) { this.notify_config_changes() } } raise_irq(type: number): void { dbg_log('Raise irq ' + h(type), LOG_VIRTIO) this.isr_status |= type this.pci.raise_irq(this.pci_id) } lower_irq(): void { dbg_log('Lower irq ', LOG_VIRTIO) this.isr_status = 0 this.pci.lower_irq(this.pci_id) } } export class VirtQueue { cpu: VirtioCPU virtio: VirtIO size: number size_supported: number mask: number enabled: boolean notify_offset: number desc_addr: number avail_addr: number avail_last_idx: number used_addr: number num_staged_replies: number fix_wrapping: boolean constructor(cpu: VirtioCPU, virtio: VirtIO, options: VirtQueue_Options) { this.cpu = cpu this.virtio = virtio // Number of entries. this.size = options.size_supported this.size_supported = options.size_supported this.mask = this.size - 1 this.enabled = false this.notify_offset = options.notify_offset this.desc_addr = 0 this.avail_addr = 0 this.avail_last_idx = 0 this.used_addr = 0 this.num_staged_replies = 0 this.fix_wrapping = false this.reset() } get_state(): any[] { const state: any[] = [] state[0] = this.size state[1] = this.size_supported state[2] = this.enabled state[3] = this.notify_offset state[4] = this.desc_addr state[5] = this.avail_addr state[6] = this.avail_last_idx state[7] = this.used_addr state[8] = this.num_staged_replies state[9] = 1 return state } set_state(state: any[]): void { this.size = state[0] this.size_supported = state[1] this.enabled = state[2] this.notify_offset = state[3] this.desc_addr = state[4] this.avail_addr = state[5] this.avail_last_idx = state[6] this.used_addr = state[7] this.num_staged_replies = state[8] this.mask = this.size - 1 this.fix_wrapping = state[9] !== 1 } reset(): void { this.enabled = false this.desc_addr = 0 this.avail_addr = 0 this.avail_last_idx = 0 this.used_addr = 0 this.num_staged_replies = 0 this.set_size(this.size_supported) } is_configured(): boolean { return !!(this.desc_addr && this.avail_addr && this.used_addr) } enable(): void { dbg_assert( this.is_configured(), 'VirtQueue must be configured before enabled', ) this.enabled = true } set_size(size: number): void { dbg_assert( (size & (size - 1)) === 0, 'VirtQueue size must be power of 2 or zero', ) dbg_assert( size <= this.size_supported, 'VirtQueue size must be within supported size', ) this.size = size this.mask = size - 1 } count_requests(): number { dbg_assert( !!this.avail_addr, 'VirtQueue addresses must be configured before use', ) if (this.fix_wrapping) { this.fix_wrapping = false this.avail_last_idx = (this.avail_get_idx() & ~this.mask) + (this.avail_last_idx & this.mask) } return (this.avail_get_idx() - this.avail_last_idx) & 0xffff } has_request(): boolean { dbg_assert( !!this.avail_addr, 'VirtQueue addresses must be configured before use', ) return this.count_requests() !== 0 } pop_request(): VirtQueueBufferChain { dbg_assert( !!this.avail_addr, 'VirtQueue addresses must be configured before use', ) dbg_assert( this.has_request(), 'VirtQueue must not pop nonexistent request', ) const desc_idx = this.avail_get_entry(this.avail_last_idx) dbg_log( 'Pop request: avail_last_idx=' + this.avail_last_idx + ' desc_idx=' + desc_idx, LOG_VIRTIO, ) const bufchain = new VirtQueueBufferChain(this, desc_idx) this.avail_last_idx = (this.avail_last_idx + 1) & 0xffff return bufchain } // Stage a buffer chain into the used ring. // Can call push_reply many times before flushing to batch replies together. // Note: this reply is not visible to driver until flush_replies is called. push_reply(bufchain: VirtQueueBufferChain): void { dbg_assert( !!this.used_addr, 'VirtQueue addresses must be configured before use', ) dbg_assert( this.num_staged_replies < this.size, 'VirtQueue replies must not exceed queue size', ) const used_idx = (this.used_get_idx() + this.num_staged_replies) & this.mask dbg_log( 'Push reply: used_idx=' + used_idx + ' desc_idx=' + bufchain.head_idx, LOG_VIRTIO, ) this.used_set_entry( used_idx, bufchain.head_idx, bufchain.length_written, ) this.num_staged_replies++ } // Makes replies visible to driver by updating the used ring idx and // firing appropriate interrupt if needed. flush_replies(): void { dbg_assert( !!this.used_addr, 'VirtQueue addresses must be configured before use', ) if (this.num_staged_replies === 0) { dbg_log('flush_replies: Nothing to flush', LOG_VIRTIO) return } dbg_log('Flushing ' + this.num_staged_replies + ' replies', LOG_VIRTIO) const old_idx = this.used_get_idx() const new_idx = (old_idx + this.num_staged_replies) & VIRTQ_IDX_MASK this.used_set_idx(new_idx) this.num_staged_replies = 0 if (this.virtio.is_feature_negotiated(VIRTIO_F_RING_EVENT_IDX)) { const used_event = this.avail_get_used_event() // Fire irq when idx values associated with the pushed reply buffers // has reached or gone past used_event. let _has_passed = old_idx <= used_event && used_event < new_idx // Has overflowed? Assumes num_staged_replies > 0. if (new_idx <= old_idx) { _has_passed = used_event < new_idx || old_idx <= used_event } // Commented out: Workaround for sometimes loading from the filesystem hangs and the emulator stays idle //if(has_passed) { this.virtio.raise_irq(VIRTIO_ISR_QUEUE) } } else { if (~this.avail_get_flags() & VIRTQ_AVAIL_F_NO_INTERRUPT) { this.virtio.raise_irq(VIRTIO_ISR_QUEUE) } } } // If using VIRTIO_F_RING_EVENT_IDX, device must tell driver when // to get notifications or else driver won't notify regularly. // If not using VIRTIO_F_RING_EVENT_IDX, driver will ignore avail_event // and notify every request regardless unless NO_NOTIFY is set (TODO implement when needed). notify_me_after(num_skipped_requests: number): void { dbg_assert( num_skipped_requests >= 0, 'Must skip a non-negative number of requests', ) // The 16 bit idx field wraps around after 2^16. const avail_event = (this.avail_get_idx() + num_skipped_requests) & 0xffff this.used_set_avail_event(avail_event) } get_descriptor(table_address: number, i: number): VirtQueueDescriptor { return { addr_low: this.cpu.read32s( table_address + i * VIRTQ_DESC_ENTRYSIZE, ), addr_high: this.cpu.read32s( table_address + i * VIRTQ_DESC_ENTRYSIZE + 4, ), len: this.cpu.read32s(table_address + i * VIRTQ_DESC_ENTRYSIZE + 8), flags: this.cpu.read16( table_address + i * VIRTQ_DESC_ENTRYSIZE + 12, ), next: this.cpu.read16( table_address + i * VIRTQ_DESC_ENTRYSIZE + 14, ), } } // Avail ring fields avail_get_flags(): number { return this.cpu.read16(this.avail_addr) } avail_get_idx(): number { return this.cpu.read16(this.avail_addr + 2) } avail_get_entry(i: number): number { return this.cpu.read16( this.avail_addr + 4 + VIRTQ_AVAIL_ENTRYSIZE * (i & this.mask), ) } avail_get_used_event(): number { return this.cpu.read16( this.avail_addr + 4 + VIRTQ_AVAIL_ENTRYSIZE * this.size, ) } // Used ring fields used_get_flags(): number { return this.cpu.read16(this.used_addr) } used_set_flags(value: number): void { this.cpu.write16(this.used_addr, value) } used_get_idx(): number { return this.cpu.read16(this.used_addr + 2) } used_set_idx(value: number): void { this.cpu.write16(this.used_addr + 2, value) } used_set_entry(i: number, desc_idx: number, length_written: number): void { this.cpu.write32( this.used_addr + 4 + VIRTQ_USED_ENTRYSIZE * i, desc_idx, ) this.cpu.write32( this.used_addr + 8 + VIRTQ_USED_ENTRYSIZE * i, length_written, ) } used_set_avail_event(value: number): void { this.cpu.write16( this.used_addr + 4 + VIRTQ_USED_ENTRYSIZE * this.size, value, ) } } // Traverses through descriptor chain starting at head_id. // Provides means to read/write to buffers represented by the descriptors. export class VirtQueueBufferChain { cpu: VirtioCPU virtio: VirtIO head_idx: number read_buffers: VirtQueueDescriptor[] read_buffer_idx: number read_buffer_offset: number length_readable: number write_buffers: VirtQueueDescriptor[] write_buffer_idx: number write_buffer_offset: number length_written: number length_writable: number constructor(virtqueue: VirtQueue, head_idx: number) { this.cpu = virtqueue.cpu this.virtio = virtqueue.virtio this.head_idx = head_idx this.read_buffers = [] // Pointers for sequential consumption via get_next_blob. this.read_buffer_idx = 0 this.read_buffer_offset = 0 this.length_readable = 0 this.write_buffers = [] // Pointers for sequential write via set_next_blob. this.write_buffer_idx = 0 this.write_buffer_offset = 0 this.length_written = 0 this.length_writable = 0 // Traverse chain to discover buffers. // - There shouldn't be an excessive amount of descriptor elements. let table_address = virtqueue.desc_addr let desc_idx = head_idx let chain_length = 0 let chain_max = virtqueue.size let writable_region = false const has_indirect_feature = this.virtio.is_feature_negotiated( VIRTIO_F_RING_INDIRECT_DESC, ) dbg_log('<<< Descriptor chain start', LOG_VIRTIO) do { const desc = virtqueue.get_descriptor(table_address, desc_idx) dbg_log( 'descriptor: idx=' + desc_idx + ' addr=' + h(desc.addr_high, 8) + ':' + h(desc.addr_low, 8) + ' len=' + h(desc.len, 8) + ' flags=' + h(desc.flags, 4) + ' next=' + h(desc.next, 4), LOG_VIRTIO, ) if (has_indirect_feature && desc.flags & VIRTQ_DESC_F_INDIRECT) { if (DEBUG && desc.flags & VIRTQ_DESC_F_NEXT) { dbg_log( 'Driver bug: has set VIRTQ_DESC_F_NEXT flag in an indirect table descriptor', LOG_VIRTIO, ) } // Carry on using indirect table, starting at first entry. table_address = desc.addr_low desc_idx = 0 chain_length = 0 chain_max = desc.len / VIRTQ_DESC_ENTRYSIZE dbg_log('start indirect', LOG_VIRTIO) continue } if (desc.flags & VIRTQ_DESC_F_WRITE) { writable_region = true this.write_buffers.push(desc) this.length_writable += desc.len } else { if (writable_region) { dbg_log( 'Driver bug: readonly buffer after writeonly buffer within chain', LOG_VIRTIO, ) break } this.read_buffers.push(desc) this.length_readable += desc.len } chain_length++ if (chain_length > chain_max) { dbg_log( 'Driver bug: descriptor chain cycle detected', LOG_VIRTIO, ) break } if (desc.flags & VIRTQ_DESC_F_NEXT) { desc_idx = desc.next } else { break } // eslint-disable-next-line no-constant-condition } while (true) dbg_log('Descriptor chain end >>>', LOG_VIRTIO) } // Reads the next blob of memory represented by the buffer chain into dest_buffer. get_next_blob(dest_buffer: Uint8Array): number { let dest_offset = 0 let remaining = dest_buffer.length while (remaining) { if (this.read_buffer_idx === this.read_buffers.length) { dbg_log( 'Device<' + this.virtio.name + '> Read more than device-readable buffers has', LOG_VIRTIO, ) break } const buf = this.read_buffers[this.read_buffer_idx] const read_address = buf.addr_low + this.read_buffer_offset let read_length = buf.len - this.read_buffer_offset if (read_length > remaining) { read_length = remaining this.read_buffer_offset += remaining } else { this.read_buffer_idx++ this.read_buffer_offset = 0 } dest_buffer.set( this.cpu.read_blob(read_address, read_length), dest_offset, ) dest_offset += read_length remaining -= read_length } return dest_offset } // Appends contents of src_buffer into the memory represented by the buffer chain. set_next_blob(src_buffer: Uint8Array): number { let src_offset = 0 let remaining = src_buffer.length while (remaining) { if (this.write_buffer_idx === this.write_buffers.length) { dbg_log( 'Device<' + this.virtio.name + '> Write more than device-writable capacity', LOG_VIRTIO, ) break } const buf = this.write_buffers[this.write_buffer_idx] const write_address = buf.addr_low + this.write_buffer_offset let write_length = buf.len - this.write_buffer_offset if (write_length > remaining) { write_length = remaining this.write_buffer_offset += remaining } else { this.write_buffer_idx++ this.write_buffer_offset = 0 } const src_end = src_offset + write_length this.cpu.write_blob( src_buffer.subarray(src_offset, src_end), write_address, ) src_offset += write_length remaining -= write_length } this.length_written += src_offset return src_offset } }