Browse Source

Implement Virtio Console device. (#949)

Implements a virtio console device. This allows
reading and writing buffers in bulk as well as
a channel to communicate terminal size updates.
Rob Blanckaert 3 months ago
parent
commit
7660cbfb64
5 changed files with 307 additions and 2 deletions
  1. 1 1
      Makefile
  2. 1 1
      debug.html
  3. 1 0
      src/browser/starter.js
  4. 6 0
      src/cpu.js
  5. 298 0
      src/virtio_console.js

+ 1 - 1
Makefile

@@ -81,7 +81,7 @@ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature
 CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \
 	   memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \
 	   acpi.js apic.js ioapic.js \
-	   state.js ne2k.js sb16.js virtio.js bus.js log.js \
+	   state.js ne2k.js sb16.js virtio.js virtio_console.js bus.js log.js \
 	   cpu.js debug.js \
 	   elf.js kernel.js
 LIB_FILES=9p.js filesystem.js jor1k.js marshall.js utf8.js

+ 1 - 1
debug.html

@@ -11,7 +11,7 @@ var CORE_FILES =
     "const.js config.js log.js lib.js buffer.js cpu.js debug.js " +
     "io.js main.js ide.js pci.js floppy.js " +
     "memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js acpi.js apic.js ioapic.js sb16.js " +
-    "ne2k.js state.js virtio.js bus.js elf.js kernel.js";
+    "ne2k.js state.js virtio.js virtio_console.js bus.js elf.js kernel.js";
 
 var BROWSER_FILES = "main.js screen.js keyboard.js mouse.js speaker.js serial.js network.js starter.js worker_bus.js print_stats.js filestorage.js";
 var LIB_FILES = "";

+ 1 - 0
src/browser/starter.js

@@ -279,6 +279,7 @@ V86.prototype.continue_init = async function(emulator, options)
     settings.preserve_mac_from_state_image = options.preserve_mac_from_state_image;
     settings.mac_address_translation = options.mac_address_translation;
     settings.cpuid_level = options.cpuid_level;
+    settings.virtio_console = options.virtio_console;
 
     if(options.network_adapter)
     {

+ 6 - 0
src/cpu.js

@@ -416,6 +416,7 @@ CPU.prototype.get_state = function()
     state[79] = this.devices.uart1;
     state[80] = this.devices.uart2;
     state[81] = this.devices.uart3;
+    state[82] = this.devices.virtio_console;
 
     return state;
 };
@@ -530,6 +531,7 @@ CPU.prototype.set_state = function(state)
     this.devices.uart1 && this.devices.uart1.set_state(state[79]);
     this.devices.uart2 && this.devices.uart2.set_state(state[80]);
     this.devices.uart3 && this.devices.uart3.set_state(state[81]);
+    this.devices.virtio_console && this.devices.virtio_console.set_state(state[82]);
 
     this.fw_value = state[62];
 
@@ -956,6 +958,10 @@ CPU.prototype.init = function(settings, device_bus)
         {
             this.devices.virtio_9p = new Virtio9p(settings.fs9p, this, device_bus);
         }
+        if(settings.virtio_console)
+        {
+            this.devices.virtio_console = new VirtioConsole(this, device_bus);
+        }
 
         if(true)
         {

+ 298 - 0
src/virtio_console.js

@@ -0,0 +1,298 @@
+
+"use strict";
+
+// https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-2900003
+
+const VIRTIO_CONSOLE_DEVICE_READY     = 0;
+const VIRTIO_CONSOLE_DEVICE_ADD       = 1;
+const VIRTIO_CONSOLE_DEVICE_REMOVE    = 2;
+const VIRTIO_CONSOLE_PORT_READY       = 3;
+const VIRTIO_CONSOLE_CONSOLE_PORT     = 4;
+const VIRTIO_CONSOLE_RESIZE           = 5;
+const VIRTIO_CONSOLE_PORT_OPEN        = 6;
+const VIRTIO_CONSOLE_PORT_NAME        = 7;
+
+const VIRTIO_CONSOLE_F_SIZE           = 0;
+const VIRTIO_CONSOLE_F_MULTIPORT      = 1;
+const VIRTIO_CONSOLE_F_EMERG_WRITE    = 2;
+
+/**
+ * @constructor
+ *
+ * @param {CPU} cpu
+ */
+function VirtioConsole(cpu, bus)
+{
+    /** @const @type {BusConnector} */
+    this.bus = bus;
+    this.rows = 25;
+    this.cols = 80;
+    this.ports = 4;
+
+    let queues = [
+        {
+            size_supported: 16,
+            notify_offset: 0,
+        },
+        {
+            size_supported: 16,
+            notify_offset: 1,
+        },
+        {
+            size_supported: 16,
+            notify_offset: 2,
+        },
+        {
+            size_supported: 16,
+            notify_offset: 3,
+        },
+    ];
+
+    for (let i = 1; i < this.ports; ++i)
+    {
+        queues.push({size_supported: 16, notify_offset: 0});
+        queues.push({size_supported: 8, notify_offset: 1});
+    }
+
+    /** @type {VirtIO} */
+    this.virtio = new VirtIO(cpu,
+    {
+        name: "virtio-console",
+        pci_id: 0x07 << 3,
+        device_id: 0x1043,
+        subsystem_device_id: 3,
+        common:
+        {
+            initial_port: 0xB800,
+            queues: queues,
+            features:
+            [
+                VIRTIO_CONSOLE_F_SIZE,
+                VIRTIO_CONSOLE_F_MULTIPORT,
+                VIRTIO_F_VERSION_1,
+            ],
+            on_driver_ok: () => {},
+        },
+        notification:
+        {
+            initial_port: 0xB900,
+            single_handler: false,
+            handlers:
+            [
+                (queue_id) =>
+                {
+                    let queue = this.virtio.queues[queue_id];
+
+                    // TODO: Full buffer looks like an empty buffer so prevent it from filling
+                    while (queue.count_requests() > queue.size - 2) queue.pop_request();
+                },
+                (queue_id) =>
+                {
+                    let queue = this.virtio.queues[queue_id];
+                    let port = queue_id > 3 ? (queue_id-3 >> 1) : 0;
+                    while(queue.has_request())
+                    {
+                        const bufchain = queue.pop_request();
+                        const buffer = new Uint8Array(bufchain.length_readable);
+                        bufchain.get_next_blob(buffer);
+                        this.bus.send("virito-" + port + "-output-bytes", buffer);
+                        this.Ack(queue_id, bufchain);
+                    }
+                },
+                (queue_id) =>
+                {
+                    if(queue_id != 2)
+                    {
+                        dbg_assert(false, "VirtioConsole Notified for wrong queue: " + queue_id +
+                            " (expected queue_id of 2)");
+                        return;
+                    }
+                    let queue = this.virtio.queues[queue_id];
+                    // Full buffer looks like an empty buffer so prevent it from filling
+                    while (queue.count_requests() > queue.size - 2) queue.pop_request();
+                },
+                (queue_id) =>
+                {
+                    if(queue_id != 3)
+                    {
+                        dbg_assert(false, "VirtioConsole Notified for wrong queue: " + queue_id +
+                            " (expected queue_id of 3)");
+                        return;
+                    }
+                    let queue = this.virtio.queues[queue_id];
+
+                    while(queue.has_request())
+                    {
+                        const bufchain = queue.pop_request();
+                        const buffer = new Uint8Array(bufchain.length_readable);
+                        bufchain.get_next_blob(buffer);
+                        
+
+                        let parts = marshall.Unmarshall(["w", "h", "h"], buffer, { offset : 0 });
+                        let port = parts[0];
+                        let event = parts[1];
+                        let value = parts[2];
+
+
+                        this.Ack(queue_id, bufchain);
+                        
+                        switch(event) {
+                            case VIRTIO_CONSOLE_DEVICE_READY:
+                                for (let i = 0; i < this.ports; ++i) {
+                                    this.SendEvent(i, VIRTIO_CONSOLE_DEVICE_ADD, 0);
+                                }                               
+                                break;
+                            case VIRTIO_CONSOLE_PORT_READY:
+                                this.Ack(queue_id, bufchain);
+                                this.SendEvent(port, VIRTIO_CONSOLE_CONSOLE_PORT, 1);
+                                this.SendName(port, "virtio-" + port);
+                                this.SendEvent(port, VIRTIO_CONSOLE_PORT_OPEN, 1);
+
+                                break;                     
+                            case VIRTIO_CONSOLE_PORT_OPEN:
+                                this.Ack(queue_id, bufchain);
+                                if (port == 0) {
+                                    this.SendWindowSize(port);
+                                }
+                                break;
+                            default:
+                                dbg_assert(false," VirtioConsole received unknown event: " + event[1]);
+                                return;
+                            
+                        }
+                    }
+                },
+            ],
+        },
+        isr_status:
+        {
+            initial_port: 0xB700,
+        },
+        device_specific:
+        {
+            initial_port: 0xB600,
+            struct:
+            [
+                {
+                    bytes: 2,
+                    name: "cols",
+                    read: () => this.cols,
+                    write: data => { /* read only */ },
+                },
+                {
+                    bytes: 2,
+                    name: "rows",
+                    read: () => this.rows,
+                    write: data => { /* read only */ },
+                },
+                {
+                    bytes: 4,
+                    name: "max_nr_ports",
+                    read: () => this.ports,
+                    write: data => { /* read only */ },
+                },
+                {
+                    bytes: 4,
+                    name: "emerg_wr",
+                    read: () => 0,
+                    write: data => { 
+                        dbg_assert(false, "Emergency write!");
+                    },
+                },
+           ]
+        },
+    });
+
+    for (let port = 0; port < this.ports; ++port) {
+        let queue = port == 0 ? 0 : port * 2 + 2;
+        this.bus.register("virtio-" + port + "-input-bytes", function(data) {
+            let queue = this.virtio.queues[0];
+            if (queue.has_request()) {
+                const bufchain = queue.pop_request();
+                this.Send(0, bufchain, new Uint8Array(data));
+            } else {
+                //TODO: Buffer
+            }
+        }, this);
+
+        this.bus.register("virtio-" + port + "-resize", function(size) {
+            this.cols = size[0];
+            this.rows = size[1];
+
+            if (this.virtio.queues[2].is_configured() && this.virtio.queues[2].has_request()) {
+                this.SendWindowSize(port);
+            }
+        }, this);
+    }
+}
+
+VirtioConsole.prototype.SendWindowSize = function(port)
+{
+    const bufchain = this.virtio.queues[2].pop_request();
+    let buf = new Uint8Array(12);
+    marshall.Marshall(["w", "h", "h", "h", "h"], [port, VIRTIO_CONSOLE_RESIZE, 0, this.rows, this.cols], buf, 0);
+    this.Send(2, bufchain, buf);
+};
+
+VirtioConsole.prototype.SendName = function(port, name)
+{
+    const bufchain = this.virtio.queues[2].pop_request();
+    let namex = new TextEncoder().encode(name);
+    let buf = new Uint8Array(8 + namex.length + 1);
+    marshall.Marshall(["w", "h", "h"], [port, VIRTIO_CONSOLE_PORT_NAME, 1], buf, 0);
+    for ( let i = 0; i < namex.length; ++i ) {
+        buf[i+8] = namex[i];
+    }
+    buf[8 + namex.length] = 0;
+    this.Send(2, bufchain, buf);
+};
+
+
+VirtioConsole.prototype.get_state = function()
+{
+    let state = [];
+
+    state[0] = this.virtio;
+    state[1] = this.rows;
+    state[2] = this.cols;
+    state[3] = this.ports;
+
+    return state;
+};
+
+VirtioConsole.prototype.set_state = function(state)
+{
+    this.virtio.set_state(state[0]);
+    this.rows = state[1];
+    this.cols = state[2];
+    this.ports = state[3];
+};
+
+VirtioConsole.prototype.Reset = function() {
+
+};
+
+VirtioConsole.prototype.SendEvent = function(port, event, value)
+{
+    let queue = this.virtio.queues[2];
+    const bufchain = queue.pop_request();
+
+    let buf = new Uint8Array(8);
+    marshall.Marshall(["w","h","h"], [port, event, value], buf, 0);
+    this.Send(2, bufchain, buf);
+};
+
+VirtioConsole.prototype.Send = function (queue_id, bufchain, blob)
+{
+    bufchain.set_next_blob(blob);
+    this.virtio.queues[queue_id].push_reply(bufchain);
+    this.virtio.queues[queue_id].flush_replies();
+};
+
+VirtioConsole.prototype.Ack = function (queue_id, bufchain)
+{
+    bufchain.set_next_blob(new Uint8Array(0));
+    this.virtio.queues[queue_id].push_reply(bufchain);
+    this.virtio.queues[queue_id].flush_replies();
+};
+