Imaginarium

IO and Drivers

The Device Model

All discovered devices live in a global tree. At the root of the tree is the root bus device (HID and CID "ROOT", see below), which is unique in having no parent.

Each device has two singly-linked lists of Functions, representing the physical and functional device layers respectively, and are used for dispatching IRPs to drivers.

Device Properties

All devices have a few fixed properties, and additional generic properties provided by drivers. The fixed properties are Hardware IDs (HIDs), Compatible IDs (CIDs), and Bus Address (ADR).

HIDs and CIDs are lists of strings which may be used to identify the device for driver selection.

The Bus Address (ADR) is an optional number with bus-specific format used for attaching to stub device objects which lack HIDs and CIDs, created by lower-level bus drivers such as the ACPI namespace enumerator.

All device properties are managed by the .properties field of the Device struct, which provides facilities to intern string property values in a device-global character bag, and add HIDs and CIDs to device-global string tables backed by that bag.

Generic properties are keyed by a combination of a GUID “namespace” and a u32 (see the properties autodocs for the namespaces), and may have one of several types.

Functions

Every device contians a stack of Functions, each created by a driver attaching to the device.

A driver will generally create its own function object, with the generic Function struct as a field, and use @fieldParentPtr to retrieve its own state when dispatching an IRP.

The stack of Functions is divided into two layers, the physical layer and the functional layer. Bus drivers attach to the physical layer, whereas function drivers attach to the functional layer. This permits bus drivers to attach even after the functional driver has claimed the device, which may occur due to coincidences in driver load order.

IRPs

IO Request Packets (IRPs) are the method by which IO operations are dispatched. An IRP is composed primarily of a stack of Entries, each containing an Operation.

When an IRP is issued, a Driver’s dispatch function is called and the driver peeks the top entry of the IRP’s stack to determine the operation it needs to perform. A driver may push a new entry to the IRP’s stack to forward an IO operation to a lower level driver (e.g. a file system driver may compute the block address to read and then push a read operation and issue it to the lower-level block storage driver). When a driver finishes performing its IO, it updates the irp’s status and returns from dispatch.

If Irp.event is initialized or a completion routine is provided in the entry, a driver may return DispatchStatus.pending from its dispatch routine to signal that asynchronous processing is being performed. When asynchronous processing is completed the driver MUST signal the event if it is initialized AND invoke the completion routine if it was provided.

Operations

Each entry of the IRP stack specifies an operation. More operations will be added as needed, but as of writing this page two operations are defined:

Set-Resources

The set_resources operation is invoked by the IO Manager after a device is enumerated and attached, and is passed the final set of resources chosen from the alternatives the driver provided in the .resources.setup union field of the Device object, if any were provided. A driver given these resources should configure the hardware to use the resources passed.

Private Dispatch

The private operation allows drivers to coordinate the use of operations not otherwise provided by the generic interface. A driver to which a private dispatch is issued should inspect the GUID id provided to determine if the operation is supported, and pass the IRP if it is not. In addition to the GUID id, a nullable opaque pointer is passed to the driver and a nullable opaque pointer is returned. For details on supported private dispatch functions, see individual driver pages.

As of writing this document, only the ACPI driver has a supported private dispatch function, which is used by the PCI driver during enumeration.

Drivers

A driver is responsible for performing IO operations described in IRPs and for performing device discovery.

A driver provides four functions in its VTable:

  • load() performs driver initialization and reports root devices (which will be made children of the root bus).
  • attach() attempts to attach to a given device which was matched by HID or CID, and returns true if a Function could be attached to the device.
  • dispatch() attempts to execute the top entry of an IRP on the given device Function.
  • deinit() unloads the driver and frees any global memory it allocated.

In addition, a driver is expected to provide a register() function, which initializes the driver structure and calls io.register_driver() so the io system can find the driver later during discovery.

A driver may call io.report_device at any time to report a fixed or newly discovered device.

The Base IO Manager

The IO Manager manages registration and enumeration of devices and drivers.

All drivers must be registered by calling io.register_driver() before io.load_drivers() can be called.

io.load_drivers() invokes the load() function of all drivers for which io.register_driver() was called.

After io.load_drivers() is called, all fixed (root) devices are marked as children of the root bus and enqueued for registration, at which point io.enumerate_devices() can be called.

io.enumerate_devices() repeatedly drains the device registration queue until no more devices are reported, and attempts to locate a driver for each device based on HID and CID before calling attach() on the located driver. If attach() returns false, another driver is attempted until either all drivers are passed or a driver returns true from attach().

After the initial io.enumerate_devices() is run, an initial device tree is printed to the debug console, and a registration thread is spawned which waits on devices to be discovered at runtime. (NOTE: Not yet implemented)