]> sigrok.org Git - libsigrok.git/commitdiff
output/vcd: support analog data, more channels, minor cleanup
authorGerhard Sittig <redacted>
Sat, 15 Apr 2017 11:06:20 +0000 (13:06 +0200)
committerGerhard Sittig <redacted>
Fri, 24 Jul 2020 07:10:27 +0000 (09:10 +0200)
Extend and rephrase the VCD output module, to support mixed signal data,
support higher channel counts, and address other minor issues.

Increase the number of VCD identifiers which can get generated. Bump the
limit from 94 to 18346 channels. Prefer single letter names for backwards
compatibility for the first channels. Use two or three letter identifiers
as needed for higher channel counts.

Add support for analog channels, and carefully organize a queue such
that timestamps and their data only get written after input data for
_all_ channels was received from the session feed. Provide IEEE754
double precision values for maximum compatibility with other VCD aware
software, although sigrok internally passes analog data with single
precision. This makes potential later adjustment transparent to external
software.

Factor out and rephrase code while we are here. This implementation
avoids glib calls where they'd hurt performance. A local pool reduces
malloc() pressure to increase throughput. String manipulation is tuned
for simplicity and reduced cost. Special code paths were added to tune
the use cases where mixed signals are not involved (immediate write to
the output text, bypassing the output module's local queue).

An srzip input implementation detail still makes the VCD output consume
lots of memory during merge sort of channels' data. See bug #1566.

Other nits got addressed in bypassing: Adjust data types. Separate the
gathering of detail information and the construction of the VCD header
text to simplify review and future maintenance. Skip VCD identifiers for
disabled channels. Emit a final timestamp to flush the last sample, and
communicate the total capture length.

Update comments. Update the copyright for recent non-trivial changes.

src/output/vcd.c

index 3de3dcde37b3a044679ed745e498433c987aa047..ec326b7eeae99d1184b9cd18f8e12607bd2923a2 100644 (file)
@@ -3,6 +3,7 @@
  *
  * Copyright (C) 2010 Uwe Hermann <uwe@hermann-uwe.de>
  * Copyright (C) 2013 Bert Vermeulen <bert@biot.com>
+ * Copyright (C) 2017-2020 Gerhard Sittig <gerhard.sittig@gmx.net>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
+/*
+ * TODO
+ * - Check the mixed signal queue for completeness and correctness.
+ * - Tune the analog "immediate write" code path for throughput.
+ * - Remove excess diagnostics when the implementation is considered
+ *   feature complete and reliable.
+ */
+
 #include <config.h>
+
+#include <ctype.h>
+#include <glib.h>
 #include <stdlib.h>
 #include <string.h>
-#include <glib.h>
+
 #include <libsigrok/libsigrok.h>
 #include "libsigrok-internal.h"
 
 #define LOG_PREFIX "output/vcd"
 
+static const int with_queue_stats = 0;
+static const int with_pool_stats = 0;
+
+struct vcd_channel_desc {
+       size_t index;
+       GString *name;
+       enum sr_channeltype type;
+       struct {
+               uint8_t logic;
+               double real;
+       } last;
+       uint64_t last_rcvd_snum;
+};
+
+/** Queued values for a given sample number. */
+struct vcd_queue_item {
+       uint64_t samplenum;     /**!< sample number, _not_ timestamp */
+       GString *values;        /**!< text of value changes */
+};
+
 struct context {
-       int num_enabled_channels;
-       uint8_t *prevsample;
+       size_t enabled_count;
+       size_t logic_count;
+       size_t analog_count;
        gboolean header_done;
        uint64_t period;
-       int *channel_index;
+       struct vcd_channel_desc *channels;
        uint64_t samplerate;
-       uint64_t samplecount;
+       GSList *free_list, *used_list;
+       size_t alloced, freed, reused, pooled;
+       GList *vcd_queue_list;
+       GList *vcd_queue_last;
+       gboolean immediate_write;
+       uint8_t *last_logic;
 };
 
+/*
+ * Construct VCD signal identifiers from a sigrok channel index. The
+ * routine returns a GString which the caller is supposed to release.
+ *
+ * There are 94 printable ASCII characters. For larger channel index
+ * numbers multiple letters get concatenated (sticking with letters).
+ *
+ * The current implementation covers these ranges:
+ * - 94 single letter identifiers
+ * - 26 ^ 2 = 676, 94 + 676 = 770 for two letter identifiers
+ * - 26 ^ 3 = 17576, 770 + 17576 = 18346 for three letter identifiers
+ *
+ * This approach can get extended as needed when support for larger
+ * channel counts is desired. Any such extension remains transparent
+ * to call sites.
+ *
+ * TODO This implementation assumes that the software will run on a
+ * machine which uses the ASCII character set. Platforms that use other
+ * representations or non-contiguous character ranges for their alphabet
+ * cannot use a simple addition, instead need to execute table lookups.
+ */
+
+#define VCD_IDENT_CHAR_MIN     '!'
+#define VCD_IDENT_CHAR_MAX     '~'
+#define VCD_IDENT_COUNT_1CHAR  (VCD_IDENT_CHAR_MAX + 1 - VCD_IDENT_CHAR_MIN)
+#define VCD_IDENT_ALPHA_MIN    'a'
+#define VCD_IDENT_ALPHA_MAX    'z'
+#define VCD_IDENT_COUNT_ALPHA  (VCD_IDENT_ALPHA_MAX + 1 - VCD_IDENT_ALPHA_MIN)
+#define VCD_IDENT_COUNT_2CHAR  (VCD_IDENT_COUNT_ALPHA * VCD_IDENT_COUNT_ALPHA)
+#define VCD_IDENT_COUNT_3CHAR  (VCD_IDENT_COUNT_2CHAR * VCD_IDENT_COUNT_ALPHA)
+#define VCD_IDENT_COUNT                (VCD_IDENT_COUNT_1CHAR + VCD_IDENT_COUNT_2CHAR + VCD_IDENT_COUNT_3CHAR)
+
+static GString *vcd_identifier(size_t idx)
+{
+       GString *symbol;
+       char c1, c2, c3;
+
+       symbol = g_string_sized_new(4);
+
+       /* First 94 channels, one printable character. */
+       if (idx < VCD_IDENT_COUNT_1CHAR) {
+               c1 = VCD_IDENT_CHAR_MIN + idx;
+               g_string_printf(symbol, "%c", c1);
+               return symbol;
+       }
+       idx -= VCD_IDENT_COUNT_1CHAR;
+
+       /* Next 676 channels, two lower case characters. */
+       if (idx < VCD_IDENT_COUNT_2CHAR) {
+               c2 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA);
+               idx /= VCD_IDENT_COUNT_ALPHA;
+               c1 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA);
+               idx /= VCD_IDENT_COUNT_ALPHA;
+               if (idx)
+                       sr_dbg("VCD identifier creation BUG (two char).");
+               g_string_printf(symbol, "%c%c", c1, c2);
+               return symbol;
+       }
+       idx -= VCD_IDENT_COUNT_2CHAR;
+
+       /* Next 17576 channels, three lower case characters. */
+       if (idx < VCD_IDENT_COUNT_3CHAR) {
+               c3 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA);
+               idx /= VCD_IDENT_COUNT_ALPHA;
+               c2 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA);
+               idx /= VCD_IDENT_COUNT_ALPHA;
+               c1 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA);
+               idx /= VCD_IDENT_COUNT_ALPHA;
+               if (idx)
+                       sr_dbg("VCD identifier creation BUG (three char).");
+               g_string_printf(symbol, "%c%c%c", c1, c2, c3);
+               return symbol;
+       }
+       idx -= VCD_IDENT_COUNT_3CHAR;
+
+       /*
+        * TODO
+        * Add combinations with more positions or larger character sets
+        * when support for more channels is required.
+        */
+       sr_dbg("VCD identifier creation ENOTSUPP (need %zu more).", idx);
+       g_string_free(symbol, TRUE);
+
+       return NULL;
+}
+
+/*
+ * Notes on the VCD text output formatting routines:
+ * - Always start new text lines when timestamps get emitted.
+ * - Optionally terminate timestamp lines when the caller asked us to.
+ * - Prepend all values with whitespace, assume they follow a timestamp
+ *   or a previously printed value. This works fine from the data point
+ *   of view for the start of new lines, as well.
+ * - Put the mandatory whitespace between real (or vector) values and
+ *   the following identifier. No whitespace for single bit values.
+ * - For real values callers need not specify "precision" nor the number
+ *   of significant digits. The Verilog VCD spec specifically picked the
+ *   "%.16g" format such that all bits of the internal presentation of
+ *   the IEEE754 floating point value get communicated between the
+ *   writer and the reader.
+ */
+
+static void append_vcd_timestamp(GString *s, double ts, gboolean lf)
+{
+
+       g_string_append_c(s, '\n');
+       g_string_append_c(s, '#');
+       g_string_append_printf(s, "%.0f", ts);
+       g_string_append_c(s, lf ? '\n' : ' ');
+}
+
+static void format_vcd_value_bit(GString *s, uint8_t bit_value, GString *id)
+{
+
+       g_string_append_c(s, bit_value ? '1' : '0');
+       g_string_append(s, id->str);
+}
+
+static void format_vcd_value_real(GString *s, double real_value, GString *id)
+{
+
+       g_string_append_c(s, 'r');
+       g_string_append_printf(s, "%.16g", real_value);
+       g_string_append_c(s, ' ');
+       g_string_append(s, id->str);
+}
+
 static int init(struct sr_output *o, GHashTable *options)
 {
        struct context *ctx;
+       size_t alloc_size;
        struct sr_channel *ch;
        GSList *l;
-       int num_enabled_channels, i;
+       size_t num_enabled, num_logic, num_analog, desc_idx;
+       struct vcd_channel_desc *desc;
 
        (void)options;
 
-       num_enabled_channels = 0;
+       /* Determine the number of involved channels. */
+       num_enabled = 0;
+       num_logic = 0;
+       num_analog = 0;
        for (l = o->sdi->channels; l; l = l->next) {
                ch = l->data;
-               if (ch->type != SR_CHANNEL_LOGIC)
-                       continue;
                if (!ch->enabled)
                        continue;
-               num_enabled_channels++;
+               if (ch->type == SR_CHANNEL_LOGIC) {
+                       num_logic++;
+               } else if (ch->type == SR_CHANNEL_ANALOG) {
+                       num_analog++;
+               } else {
+                       continue;
+               }
+               num_enabled++;
        }
-       if (num_enabled_channels > 94) {
-               sr_err("VCD only supports 94 channels.");
+       if (num_enabled > VCD_IDENT_COUNT) {
+               sr_err("Only up to %d VCD signals supported.", VCD_IDENT_COUNT);
                return SR_ERR;
        }
 
-       ctx = g_malloc0(sizeof(struct context));
+       /* Allocate space for channel descriptions. */
+       ctx = g_malloc0(sizeof(*ctx));
        o->priv = ctx;
-       ctx->num_enabled_channels = num_enabled_channels;
-       ctx->channel_index = g_malloc(sizeof(int) * ctx->num_enabled_channels);
+       ctx->enabled_count = num_enabled;
+       ctx->logic_count = num_logic;
+       ctx->analog_count = num_analog;
+       alloc_size = sizeof(ctx->channels[0]) * ctx->enabled_count;
+       ctx->channels = g_malloc0(alloc_size);
 
-       /* Once more to map the enabled channels. */
-       for (i = 0, l = o->sdi->channels; l; l = l->next) {
+       /*
+        * Reiterate input descriptions, to fill in output descriptions.
+        * Map channel indices, and assign symbols to VCD channels.
+        */
+       desc_idx = 0;
+       for (l = o->sdi->channels; l; l = l->next) {
                ch = l->data;
-               if (ch->type != SR_CHANNEL_LOGIC)
-                       continue;
                if (!ch->enabled)
                        continue;
-               ctx->channel_index[i++] = ch->index;
+               desc = &ctx->channels[desc_idx];
+               desc->index = ch->index;
+               desc->name = vcd_identifier(desc_idx);
+               desc->type = ch->type;
+               /*
+                * Make sure to _not_ match next time, to have initial
+                * values dumped when the first sample gets received.
+                */
+               if (desc->type == SR_CHANNEL_LOGIC && num_logic) {
+                       num_logic--;
+                       desc->last.logic = ~0;
+               } else if (desc->type == SR_CHANNEL_ANALOG && num_analog) {
+                       num_analog--;
+                       /* "Construct" NaN, avoid a compile time error. */
+                       desc->last.real = 0.0;
+                       desc->last.real = 0.0 / desc->last.real;
+               } else {
+                       g_string_free(desc->name, TRUE);
+                       memset(desc, 0, sizeof(*desc));
+                       continue;
+               }
+               desc_idx++;
        }
 
+       /*
+        * Keep channel counts at hand, and a flag which allows to tune
+        * for special cases' speedup in .receive().
+        */
+       ctx->immediate_write = FALSE;
+       if (ctx->analog_count == 0)
+               ctx->immediate_write = TRUE;
+       if (ctx->logic_count == 0 && ctx->analog_count == 1)
+               ctx->immediate_write = TRUE;
+
+       /*
+        * Keep a copy of the last logic data bitmap around. To avoid
+        * iterating over individual bits when nothing in the set has
+        * changed. The overhead of two byte array compares should
+        * outweight the tenfold bit count compared to byte counts.
+        */
+       alloc_size = (ctx->logic_count + 7) / 8;
+       ctx->last_logic = g_malloc0(alloc_size);
+       if (ctx->logic_count && !ctx->last_logic)
+               return SR_ERR_MALLOC;
+
        return SR_OK;
 }
 
@@ -86,7 +309,7 @@ static int init(struct sr_output *o, GHashTable *options)
 static uint64_t get_timescale_freq(uint64_t samplerate)
 {
        uint64_t timescale;
-       int max_up_scale;
+       size_t max_up_scale;
 
        /* Go to the next full decade. */
        timescale = 1;
@@ -110,6 +333,7 @@ static uint64_t get_timescale_freq(uint64_t samplerate)
        return timescale;
 }
 
+/* Emit a VCD file header. */
 static GString *gen_header(const struct sr_output *o)
 {
        struct context *ctx;
@@ -118,78 +342,664 @@ static GString *gen_header(const struct sr_output *o)
        GString *header;
        GSList *l;
        time_t t;
-       int num_channels, i;
+       size_t num_channels, i;
        char *samplerate_s, *frequency_s, *timestamp;
+       struct vcd_channel_desc *desc;
+       char *type_text, *size_text;
+       int ret;
 
        ctx = o->priv;
-       header = g_string_sized_new(512);
-       num_channels = g_slist_length(o->sdi->channels);
 
-       /* timestamp */
+       /* Get channel count, and samplerate if not done yet. */
+       num_channels = g_slist_length(o->sdi->channels);
+       if (!ctx->samplerate) {
+               ret = sr_config_get(o->sdi->driver, o->sdi, NULL,
+                       SR_CONF_SAMPLERATE, &gvar);
+               if (ret == SR_OK) {
+                       ctx->samplerate = g_variant_get_uint64(gvar);
+                       g_variant_unref(gvar);
+               }
+       }
+       ctx->period = get_timescale_freq(ctx->samplerate);
        t = time(NULL);
        timestamp = g_strdup(ctime(&t));
-       timestamp[strlen(timestamp) - 1] = 0;
+       timestamp[strlen(timestamp) - 1] = '\0';
+       samplerate_s = NULL;
+       if (ctx->samplerate)
+               samplerate_s = sr_samplerate_string(ctx->samplerate);
+       frequency_s = sr_period_string(1, ctx->period);
+
+       /* Construct the VCD output file header. */
+       header = g_string_sized_new(512);
        g_string_printf(header, "$date %s $end\n", timestamp);
+       g_string_append_printf(header, "$version %s %s $end\n",
+               PACKAGE_NAME, sr_package_version_string_get());
+       g_string_append_printf(header, "$comment\n");
+       g_string_append_printf(header,
+               "  Acquisition with %zu/%zu channels%s%s\n",
+               ctx->enabled_count, num_channels,
+               samplerate_s ? " at " : "", samplerate_s ? : "");
+       g_string_append_printf(header, "$end\n");
+       g_string_append_printf(header, "$timescale %s $end\n", frequency_s);
+
+       /* List generated VCD signals within a scope. */
+       g_string_append_printf(header, "$scope module %s $end\n", PACKAGE_NAME);
+       i = 0;
+       for (l = o->sdi->channels; l; l = l->next) {
+               ch = l->data;
+               if (!ch->enabled)
+                       continue;
+               desc = &ctx->channels[i++];
+               if (desc->type == SR_CHANNEL_LOGIC) {
+                       type_text = "wire";
+                       size_text = "1";
+               } else if (desc->type == SR_CHANNEL_ANALOG) {
+                       type_text = "real";
+                       size_text = "64";
+               } else {
+                       i--;
+                       continue;
+               }
+               g_string_append_printf(header, "$var %s %s %s %s $end\n",
+                       type_text, size_text, desc->name->str, ch->name);
+       }
+       g_string_append(header, "$upscope $end\n");
+
+       g_string_append(header, "$enddefinitions $end\n");
+
        g_free(timestamp);
+       g_free(samplerate_s);
+       g_free(frequency_s);
 
-       /* generator */
-       g_string_append_printf(header, "$version %s %s $end\n",
-                       PACKAGE_NAME, sr_package_version_string_get());
-       g_string_append_printf(header, "$comment\n  Acquisition with "
-                       "%d/%d channels", ctx->num_enabled_channels, num_channels);
+       return header;
+}
 
-       if (ctx->samplerate == 0) {
-               if (sr_config_get(o->sdi->driver, o->sdi, NULL, SR_CONF_SAMPLERATE,
-                               &gvar) == SR_OK) {
-                       ctx->samplerate = g_variant_get_uint64(gvar);
-                       g_variant_unref(gvar);
+/*
+ * Gets called when a session feed packet was received. Either creates
+ * a VCD file header (once in the output module's lifetime), or an empty
+ * GString. Callers will append the text representation of sample data
+ * to that string as needed.
+ */
+static GString *chk_header(const struct sr_output *o)
+{
+       struct context *ctx;
+       GString *s;
+
+       ctx = o->priv;
+
+       if (!ctx->header_done) {
+               ctx->header_done = TRUE;
+               s = gen_header(o);
+       } else {
+               s = g_string_sized_new(512);
+       }
+
+       return s;
+}
+
+/*
+ * Helpers to "merge sort" sample data that we have received in chunks
+ * at different times in different code paths. Queue the data until we
+ * have seen samples from all involved channels for a given samplenumber.
+ * Data for a given sample number can only get emitted when we are sure
+ * no other channel's data can arrive any more.
+ */
+
+static struct vcd_queue_item *queue_alloc_item(struct context *ctx, uint64_t snum)
+{
+       GSList *node;
+       struct vcd_queue_item *item;
+
+       /* Get an item from the free list if available. */
+       node = ctx->free_list;
+       if (node) {
+               ctx->reused++;
+
+               /* Unlink GSList node from the free list. */
+               ctx->free_list = node->next;
+               node->next = NULL;
+               item = node->data;
+               node->data = NULL;
+
+               /* Setup content of the item. */
+               item->samplenum = snum;
+               if (!item->values)
+                       item->values = g_string_sized_new(32);
+               else
+                       g_string_truncate(item->values, 0);
+
+               /* Keep GSList node in the used list (avoid free/alloc). */
+               node->next = ctx->used_list;
+               ctx->used_list = node;
+
+               return item;
+       }
+
+       /* Dynamic allocation of an item. */
+       ctx->alloced++;
+       item = g_malloc0(sizeof(*item));
+       if (!item)
+               return NULL;
+       item->samplenum = snum;
+       item->values = g_string_sized_new(32);
+
+       /* Create a used list item, to later move to the free list. */
+       ctx->used_list = g_slist_prepend(ctx->used_list, item);
+
+       return item;
+}
+
+static void queue_free_item(struct context *ctx, struct vcd_queue_item *item)
+{
+       GSList *node;
+
+       /*
+        * Put item back into the free list. We can assume to find a
+        * used list node, it got allocated when the item was acquired.
+        */
+       node = ctx->used_list;
+       if (node) {
+               ctx->pooled++;
+
+               ctx->used_list = node->next;
+               node->next = NULL;
+               node->data = item;
+
+               item->samplenum = 0;
+               g_string_truncate(item->values, 0);
+
+               node->next = ctx->free_list;
+               ctx->free_list = node;
+
+               return;
+       }
+
+       /*
+        * Release dynamically allocated resources. Could also be used
+        * to release free list items when the use list is empty.
+        */
+       ctx->freed++;
+       if (item->values)
+               g_string_free(item->values, TRUE);
+       g_free(item);
+}
+
+static void queue_drain_pool_cb(gpointer data, gpointer cb_data)
+{
+       struct context *ctx;
+       struct vcd_queue_item *item;
+
+       item = data;
+       ctx = cb_data;
+       queue_free_item(ctx, item);
+}
+
+static void queue_drain_pool(struct context *ctx)
+{
+       GSList *list;
+
+       /*
+        * Grab the list and "empty" the context member. Then
+        * iterate over the items, have dymamic memory released.
+        * Then free the GSList nodes (but not their data parts).
+        * Do this for the used and the free lists.
+        */
+       list = ctx->used_list;
+       ctx->used_list = NULL;
+       g_slist_foreach(list, queue_drain_pool_cb, ctx);
+       g_slist_free(list);
+
+       list = ctx->free_list;
+       ctx->free_list = NULL;
+       g_slist_foreach(list, queue_drain_pool_cb, ctx);
+       g_slist_free(list);
+}
+
+static int cmp_snum(gconstpointer l, gconstpointer d)
+{
+       const struct vcd_queue_item *list_item;
+       const uint64_t *snum_ptr;
+
+       list_item = l;
+       snum_ptr = d;
+       if (list_item->samplenum > *snum_ptr)
+               return +1;
+       if (list_item->samplenum < *snum_ptr)
+               return -1;
+       return 0;
+}
+
+static int cmp_items(gconstpointer a, gconstpointer b)
+{
+       const struct vcd_queue_item *item_a, *item_b;
+
+       item_a = a;
+       item_b = b;
+       if (item_a->samplenum > item_b->samplenum)
+               return +1;
+       if (item_a->samplenum < item_b->samplenum)
+               return -1;
+       return 0;
+}
+
+/*
+ * Position the current pointer of the VCD value queue to a specific
+ * sample number. Create a new queue item when needed. The logic assumes
+ * a specific use pattern: Reception of striped sample data for channels
+ * and processing in strict order of sample numbers within a channel.
+ * Lower sample numbers near the start of the queue when channels change
+ * between session feed packets, before another linear sequence follows.
+ *
+ * Naive use of convenience glib routines would severely lose performance.
+ * That's why custom code is used, which is as complex as it needs to be,
+ * yet shall execute faster than a simpler implementation. For trivial
+ * cases (logic only, one analog channel only) this queue is bypassed.
+ */
+static int queue_samplenum(struct context *ctx, uint64_t snum)
+{
+       struct vcd_queue_item *item, *add_item;
+       GList *walk_list, *after_snum, *before_snum, *add_list;
+       GList *last;
+       gboolean add_after_last, do_search;
+
+       /* Already at that position? */
+       item = ctx->vcd_queue_last ? ctx->vcd_queue_last->data : NULL;
+       if (item && item->samplenum == snum)
+               return SR_OK;
+
+       /*
+        * Search after the current position in the remaining queue. The
+        * custom code uses the queue's being sorted by sample number.
+        * Narrow down a later insert position as much as possible. This
+        * avoids linear search in huge spaces later on.
+        */
+       last = NULL;
+       add_after_last = FALSE;
+       after_snum = NULL;
+       before_snum = NULL;
+       walk_list = ctx->vcd_queue_last;
+       while (walk_list) {
+               item = walk_list->data;
+               if (!item)
+                       break;
+               if (item->samplenum == snum) {
+                       ctx->vcd_queue_last = walk_list;
+                       return SR_OK;
+               }
+               last = walk_list;
+               if (item->samplenum < snum)
+                       before_snum = walk_list;
+               if (item->samplenum > snum) {
+                       after_snum = walk_list;
+                       break;
                }
+               if (!walk_list->next)
+                       add_after_last = TRUE;
+               walk_list = walk_list->next;
        }
-       if (ctx->samplerate != 0) {
-               samplerate_s = sr_samplerate_string(ctx->samplerate);
-               g_string_append_printf(header, " at %s", samplerate_s);
-               g_free(samplerate_s);
+
+       /*
+        * No exact match at or beyond the current position. Run another
+        * search from the start of the queue, again restrict the space
+        * which is searched, and narrow down the insert position when
+        * no match is found.
+        *
+        * If the searched sample number is larger than any we have seen
+        * before, or was in the above covered range but was not found,
+        * then we know that another queue item needs to get added, and
+        * where to put it. In that case we need not iterate the earlier
+        * list items.
+        */
+       walk_list = ctx->vcd_queue_list;
+       do_search = TRUE;
+       if (add_after_last)
+               do_search = FALSE;
+       if (before_snum)
+               do_search = FALSE;
+       while (do_search && walk_list && walk_list != ctx->vcd_queue_last) {
+               item = walk_list->data;
+               if (!item)
+                       break;
+               if (item->samplenum == snum) {
+                       ctx->vcd_queue_last = walk_list;
+                       return SR_OK;
+               }
+               if (item->samplenum < snum)
+                       before_snum = walk_list;
+               if (item->samplenum > snum) {
+                       after_snum = walk_list;
+                       break;
+               }
+               walk_list = walk_list->next;
        }
-       g_string_append_printf(header, "\n$end\n");
 
-       /* timescale */
-       ctx->period = get_timescale_freq(ctx->samplerate);
-       frequency_s = sr_period_string(1, ctx->period);
-       g_string_append_printf(header, "$timescale %s $end\n", frequency_s);
-       g_free(frequency_s);
+       /*
+        * The complete existing queue was exhausted, no exact match was
+        * found. A new queue item must get inserted. Identify a good
+        * position where to start searching for the exact position to
+        * link the new item to the list. Assume that the combination of
+        * the glib routine's list traversal and the sample number check
+        * in the callback is expensive, reduce the amount of work done.
+        *
+        * If we have seen an item with a larger sample number than the
+        * wanted, check its immediate predecessor. If this has a smaller
+        * sample number, then we found a perfect location to insert the
+        * new item. If we know that the new item must be inserted after
+        * the last traversed queue item, start there.
+        */
+       if (!before_snum) do {
+               if (add_after_last)
+                       break;
+               if (!after_snum)
+                       break;
+               walk_list = after_snum->prev;
+               if (!walk_list)
+                       break;
+               item = walk_list->data;
+               if (!item)
+                       break;
+               if (item->samplenum == snum) {
+                       ctx->vcd_queue_last = walk_list;
+                       return SR_OK;
+               }
+               if (item->samplenum < snum)
+                       before_snum = walk_list;
+       } while (0);
+       add_list = add_after_last ? last : before_snum;
+       if (!add_list) {
+               walk_list = ctx->vcd_queue_list;
+               while (walk_list) {
+                       item = walk_list->data;
+                       if (!item)
+                               break;
+                       if (item->samplenum == snum) {
+                               ctx->vcd_queue_last = walk_list;
+                               return SR_OK;
+                       }
+                       if (item->samplenum > snum) {
+                               after_snum = walk_list;
+                               break;
+                       }
+                       add_list = walk_list;
+                       walk_list = walk_list->next;
+               }
+       }
+       if (add_list && (item = add_list->data) && item->samplenum == snum) {
+               ctx->vcd_queue_last = add_list;
+               return SR_OK;
+       }
 
-       /* scope */
-       g_string_append_printf(header, "$scope module %s $end\n", PACKAGE_NAME);
+       /*
+        * Create a new queue item for the so far untracked sample
+        * number. Immediately search for the inserted position (is
+        * unfortunately not returned from the insert call), and
+        * cache that position for subsequent lookups.
+        */
+       if (with_queue_stats)
+               sr_dbg("%s(), queue nr %" PRIu64, __func__, snum);
+       add_item = queue_alloc_item(ctx, snum);
+       if (!add_item)
+               return SR_ERR_MALLOC;
+       if (!add_list)
+               add_list = ctx->vcd_queue_list;
+       if (add_list && add_list->prev)
+               add_list = add_list->prev;
+       walk_list = g_list_insert_sorted(add_list, add_item, cmp_items);
+       if (!walk_list->prev)
+               ctx->vcd_queue_list = walk_list;
+       walk_list = g_list_find_custom(walk_list, &snum, cmp_snum);
+       item = walk_list ? walk_list->data : NULL;
+       if (item && item->samplenum == snum) {
+               ctx->vcd_queue_last = walk_list;
+       }
+       return SR_OK;
+}
 
-       /* Wires / channels */
-       for (i = 0, l = o->sdi->channels; l; l = l->next) {
-               ch = l->data;
-               if (ch->type != SR_CHANNEL_LOGIC)
+/*
+ * Prepare to append another text fragment for a value change to the
+ * queue item which corresponds to the current sample number. Return
+ * the GString which the caller then will append to.
+ */
+static GString *queue_value_text_prep(struct context *ctx)
+{
+       struct vcd_queue_item *item;
+       GString *buff;
+
+       /* Cope with not-yet-positioned write pointers. */
+       item = ctx->vcd_queue_last ? ctx->vcd_queue_last->data : NULL;
+       if (!item)
+               return NULL;
+
+       /* Create a GString if not done already. */
+       buff = item->values;
+       if (!buff) {
+               buff = g_string_sized_new(20);
+               item->values = buff;
+       }
+
+       /* Separate items with spaces (if previous content is present). */
+       if (buff->len)
+               g_string_append_c(buff, ' ');
+
+       return buff;
+}
+
+static double snum_to_ts(struct context *ctx, uint64_t snum)
+{
+       double ts;
+
+       ts = (double)snum;
+       ts /= ctx->samplerate;
+       ts *= ctx->period;
+
+       return ts;
+}
+
+/*
+ * Unqueue one item of the VCD values queue which corresponds to one
+ * sample number. Append all of the text to the passed in GString.
+ */
+static int unqueue_item(struct context *ctx,
+       struct vcd_queue_item *item, GString *s)
+{
+       double ts;
+       GString *buff;
+       gboolean is_empty;
+
+       /*
+        * Start the sample number's string with the timestamp. Append
+        * all value changes. Terminate lines for items which have a
+        * timestamp but no value changes, assuming this is the last
+        * entry which corresponds to SR_DF_END.
+        */
+       ts = snum_to_ts(ctx, item->samplenum);
+       buff = item->values;
+       is_empty = !buff || !buff->len || !buff->str || !*buff->str;
+       append_vcd_timestamp(s, ts, is_empty);
+       if (!is_empty)
+               g_string_append(s, buff->str);
+
+       return SR_OK;
+}
+
+/*
+ * Get the last sample number which logic data was received for. This
+ * implementation assumes that all logic channels get received within
+ * exactly one packet of corresponding unitsize.
+ */
+static uint64_t get_last_snum_logic(struct context *ctx)
+{
+       size_t i;
+       struct vcd_channel_desc *desc;
+
+       for (i = 0; i < ctx->enabled_count; i++) {
+               desc = &ctx->channels[i];
+               if (desc->type != SR_CHANNEL_LOGIC)
                        continue;
-               if (!ch->enabled)
+               return desc->last_rcvd_snum;
+       }
+
+       return 0;
+}
+
+/*
+ * Update the last sample number which logic data was received for.
+ */
+static void upd_last_snum_logic(struct context *ctx, uint64_t inc)
+{
+       size_t i;
+       struct vcd_channel_desc *desc;
+
+       for (i = 0; i < ctx->enabled_count; i++) {
+               desc = &ctx->channels[i];
+               if (desc->type != SR_CHANNEL_LOGIC)
                        continue;
-               g_string_append_printf(header, "$var wire 1 %c %s $end\n",
-                               (char)('!' + i), ch->name);
-               i++;
+               desc->last_rcvd_snum += inc;
        }
+}
 
-       g_string_append(header, "$upscope $end\n$enddefinitions $end\n");
+/*
+ * Get and update the last sample number which analog data was received
+ * for on a specific channel (which the caller already has identified).
+ */
 
-       return header;
+static uint64_t get_last_snum_analog(struct vcd_channel_desc *desc)
+{
+
+       return desc->last_rcvd_snum;
+}
+
+static void upd_last_snum_analog(struct vcd_channel_desc *desc, uint64_t inc)
+{
+
+       if (!desc)
+               return;
+       desc->last_rcvd_snum += inc;
+}
+
+/*
+ * Determine the maximum sample number which data from all involved
+ * channels was received for.
+ */
+static uint64_t get_max_snum_export(struct context *ctx)
+{
+       uint64_t snum;
+       size_t i;
+       struct vcd_channel_desc *desc;
+
+       snum = ~UINT64_C(0);
+       for (i = 0; i < ctx->enabled_count; i++) {
+               desc = &ctx->channels[i];
+               if (snum > desc->last_rcvd_snum)
+                       snum = desc->last_rcvd_snum;
+       }
+
+       return snum;
+}
+
+/*
+ * Determine the maximum sample number of any channel we may have
+ * received data for. Then pretend we had seen that number of samples
+ * on all channels. Such that the next export can flush all previously
+ * queued data up to and including the final number, which serves as
+ * some kind of termination of the VCD output data.
+ */
+static uint64_t get_max_snum_flush(struct context *ctx)
+{
+       uint64_t snum;
+       size_t i;
+       struct vcd_channel_desc *desc;
+
+       /* Determine the maximum sample number. */
+       snum = 0;
+       for (i = 0; i < ctx->enabled_count; i++) {
+               desc = &ctx->channels[i];
+               if (snum < desc->last_rcvd_snum)
+                       snum = desc->last_rcvd_snum;
+       }
+
+       /* Record that number as "seen" with all channels. */
+       for (i = 0; i < ctx->enabled_count; i++) {
+               desc = &ctx->channels[i];
+               desc->last_rcvd_snum = snum + 1;
+       }
+
+       return snum;
+}
+
+/*
+ * Pass all queued value changes when we are certain we have received
+ * data from all channels.
+ */
+static int write_completed_changes(struct context *ctx, GString *out)
+{
+       uint64_t upto_snum;
+       GList **listref, *node;
+       struct vcd_queue_item *item;
+       int rc;
+       size_t dumped;
+
+       /* Determine the number which all data was received for so far. */
+       upto_snum = get_max_snum_export(ctx);
+       if (with_queue_stats)
+               sr_spew("%s(), check up to %" PRIu64, __func__, upto_snum);
+
+       /*
+        * Forward and consume those items from the head of the list
+        * which we completely have accumulated and are certain about.
+        */
+       dumped = 0;
+       listref = &ctx->vcd_queue_list;
+       while (*listref) {
+               /* Find items before the targetted sample number. */
+               node = *listref;
+               item = node->data;
+               if (!item)
+                       break;
+               if (item->samplenum >= upto_snum)
+                       break;
+
+               /*
+                * Unlink the item from the list. Void cached positions.
+                * Append its timestamp and values to the caller's text.
+                */
+               dumped++;
+               if (with_queue_stats)
+                       sr_dbg("%s(), dump nr %" PRIu64,
+                               __func__, item->samplenum);
+               if (ctx->vcd_queue_last == node)
+                       ctx->vcd_queue_last = NULL;
+               *listref = g_list_remove_link(*listref, node);
+               rc = unqueue_item(ctx, item, out);
+               queue_free_item(ctx, item);
+               if (rc != SR_OK)
+                       return rc;
+       }
+
+       return SR_OK;
 }
 
-static int receive(const struct sr_output *o, const struct sr_datafeed_packet *packet,
-               GString **out)
+/* Get packets from the session feed, generate output text. */
+static int receive(const struct sr_output *o,
+       const struct sr_datafeed_packet *packet, GString **out)
 {
+       struct context *ctx;
        const struct sr_datafeed_meta *meta;
        const struct sr_datafeed_logic *logic;
+       const struct sr_datafeed_analog *analog;
        const struct sr_config *src;
        GSList *l;
-       struct context *ctx;
-       unsigned int i;
-       int p, curbit, prevbit, index;
-       uint8_t *sample;
-       gboolean timestamp_written;
+       struct vcd_channel_desc *desc;
+       uint64_t snum_curr;
+       size_t count, index, p, unit_size;
+       gboolean changed;
+       GString *s_val;
+       uint8_t *sample, *last_logic, prevbit, curbit;
+       GSList *channels;
+       struct sr_channel *channel;
+       int rc;
+       float *floats, value;
+       double ts;
 
        *out = NULL;
        if (!o || !o->priv)
@@ -207,25 +1017,38 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p
                }
                break;
        case SR_DF_LOGIC:
-               logic = packet->payload;
+               *out = chk_header(o);
 
-               if (!ctx->header_done) {
-                       *out = gen_header(o);
-                       ctx->header_done = TRUE;
-               } else {
-                       *out = g_string_sized_new(512);
-               }
+               logic = packet->payload;
+               sample = logic->data;
+               unit_size = logic->unitsize;
+               count = logic->length / unit_size;
+               snum_curr = get_last_snum_logic(ctx);
+               upd_last_snum_logic(ctx, count);
 
-               if (!ctx->prevsample) {
-                       /* Can't allocate this until we know the stream's unitsize. */
-                       ctx->prevsample = g_malloc0(logic->unitsize);
-               }
+               last_logic = ctx->last_logic;
+               while (count--) {
+                       /* Check whether any logic value has changed. */
+                       changed = memcmp(last_logic, sample, unit_size) != 0;
+                       changed |= snum_curr == 0;
+                       if (changed)
+                               memcpy(last_logic, sample, unit_size);
 
-               for (i = 0; i <= logic->length - logic->unitsize; i += logic->unitsize) {
-                       sample = logic->data + i;
-                       timestamp_written = FALSE;
+                       /*
+                        * Start or continue tracking that sample number.
+                        * Avoid string copies for logic-only setups.
+                        */
+                       if (changed) {
+                               if (ctx->immediate_write) {
+                                       ts = snum_to_ts(ctx, snum_curr);
+                                       append_vcd_timestamp(*out, ts, FALSE);
+                               } else {
+                                       queue_samplenum(ctx, snum_curr);
+                               }
+                       }
 
-                       for (p = 0; p < ctx->num_enabled_channels; p++) {
+                       /* Iterate over individual logic channels. */
+                       for (p = 0; changed && p < ctx->enabled_count; p++) {
                                /*
                                 * TODO Check whether the mapping from
                                 * data image positions to channel numbers
@@ -234,44 +1057,118 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p
                                 * bits of enabled channels, and leaves no
                                 * room for positions of disabled channels.
                                 */
-                               /* index = ctx->channel_index[p]; */
-                               index = p;
-
-                               curbit = ((unsigned)sample[index / 8]
-                                               >> (index % 8)) & 1;
-                               prevbit = ((unsigned)ctx->prevsample[index / 8]
-                                               >> (index % 8)) & 1;
+                               desc = &ctx->channels[p];
+                               if (desc->type != SR_CHANNEL_LOGIC)
+                                       continue;
+                               index = desc->index;
+                               prevbit = desc->last.logic;
 
-                               /* VCD only contains deltas/changes of signals. */
-                               if (prevbit == curbit && ctx->samplecount > 0)
+                               /* Skip over unchanged values. */
+                               curbit = sample[index / 8];
+                               curbit = (curbit & (1 << (index % 8))) ? 1 : 0;
+                               if (snum_curr != 0 && prevbit == curbit)
                                        continue;
+                               desc->last.logic = curbit;
 
-                               /* Output timestamp of subsequent signal changes. */
-                               if (!timestamp_written)
-                                       g_string_append_printf(*out, "#%.0f",
-                                               (double)ctx->samplecount /
-                                                       ctx->samplerate * ctx->period);
+                               /*
+                                * Queue, or immediately emit the text for
+                                * the observed value change.
+                                */
+                               if (ctx->immediate_write) {
+                                       g_string_append_c(*out, ' ');
+                                       s_val = *out;
+                               } else {
+                                       s_val = queue_value_text_prep(ctx);
+                                       if (!s_val)
+                                               break;
+                               }
+                               format_vcd_value_bit(s_val, curbit, desc->name);
+                       }
 
-                               /* Output which signal changed to which value. */
-                               g_string_append_c(*out, ' ');
-                               g_string_append_c(*out, '0' + curbit);
-                               g_string_append_c(*out, '!' + p);
+                       /* Advance to next set of logic samples. */
+                       snum_curr++;
+                       sample += unit_size;
+               }
+               write_completed_changes(ctx, *out);
+               break;
+       case SR_DF_ANALOG:
+               *out = chk_header(o);
 
-                               timestamp_written = TRUE;
-                       }
+               /*
+                * This implementation expects one analog packet per
+                * individual channel, with a number of samples each.
+                * Lookup the VCD output channel description.
+                */
+               analog = packet->payload;
+               count = analog->num_samples;
+               channels = analog->meaning->channels;
+               if (g_slist_length(channels) != 1) {
+                       sr_err("Analog packets must be single-channel.");
+                       return SR_ERR_ARG;
+               }
+               channel = g_slist_nth_data(channels, 0);
+               desc = NULL;
+               for (index = 0; index < ctx->enabled_count; index++) {
+                       desc = &ctx->channels[index];
+                       if ((int)desc->index == channel->index)
+                               break;
+               }
+               if (!desc)
+                       return SR_OK;
+               if (desc->type != SR_CHANNEL_ANALOG)
+                       return SR_ERR;
+               snum_curr = get_last_snum_analog(desc);
+               upd_last_snum_analog(desc, count);
 
-                       if (timestamp_written)
-                               g_string_append_c(*out, '\n');
+               /*
+                * Convert incoming data to an array of single precision
+                * floating point values.
+                */
+               floats = g_try_malloc(sizeof(*floats) * analog->num_samples);
+               if (!floats)
+                       return SR_ERR_MALLOC;
+               rc = sr_analog_to_float(analog, floats);
+               if (rc != SR_OK) {
+                       g_free(floats);
+                       return rc;
+               }
 
-                       ctx->samplecount++;
-                       memcpy(ctx->prevsample, sample, logic->unitsize);
+               /*
+                * Check for changes in the channel's values. Have the
+                * sample number's timestamp and new value printed when
+                * the value has changed.
+                */
+               for (index = 0; index < count; index++) {
+                       /* Check for changes in the channel's values. */
+                       value = floats[index];
+                       changed = value != desc->last.real;
+                       changed |= snum_curr + index == 0;
+                       if (!changed)
+                               continue;
+                       desc->last.real = value;
+
+                       /* Queue, or emit the timestamp and the new value. */
+                       if (ctx->immediate_write) {
+                               ts = snum_to_ts(ctx, snum_curr + index);
+                               append_vcd_timestamp(*out, ts, FALSE);
+                               s_val = *out;
+                       } else {
+                               queue_samplenum(ctx, snum_curr + index);
+                               s_val = queue_value_text_prep(ctx);
+                       }
+                       format_vcd_value_real(s_val, value, desc->name);
                }
+
+               g_free(floats);
+               write_completed_changes(ctx, *out);
                break;
        case SR_DF_END:
-               /* Write final timestamp as length indicator. */
-               *out = g_string_sized_new(512);
-               g_string_printf(*out, "#%.0f\n",
-                               (double)ctx->samplecount / ctx->samplerate * ctx->period);
+               *out = chk_header(o);
+               /* Push the final timestamp as length indicator. */
+               snum_curr = get_max_snum_flush(ctx);
+               queue_samplenum(ctx, snum_curr);
+               /* Flush previously queued value changes. */
+               write_completed_changes(ctx, *out);
                break;
        }
 
@@ -281,13 +1178,26 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p
 static int cleanup(struct sr_output *o)
 {
        struct context *ctx;
+       struct vcd_channel_desc *desc;
 
        if (!o || !o->priv)
                return SR_ERR_ARG;
 
        ctx = o->priv;
-       g_free(ctx->prevsample);
-       g_free(ctx->channel_index);
+
+       if (with_pool_stats)
+               sr_info("STATS: alloc/reuse %zu/%zu, pool/free %zu/%zu",
+                       ctx->alloced, ctx->reused, ctx->pooled, ctx->freed);
+       queue_drain_pool(ctx);
+       if (with_pool_stats)
+               sr_info("STATS: alloc/reuse %zu/%zu, pool/free %zu/%zu",
+                       ctx->alloced, ctx->reused, ctx->pooled, ctx->freed);
+
+       while (ctx->enabled_count--) {
+               desc = &ctx->channels[ctx->enabled_count];
+               g_string_free(desc->name, TRUE);
+       }
+       g_free(ctx->channels);
        g_free(ctx);
 
        return SR_OK;