]> sigrok.org Git - libsigrok.git/blobdiff - src/input/vcd.c
input/vcd: silence printf() format compiler warning
[libsigrok.git] / src / input / vcd.c
index 88e17f8b8ba4fb23c622edd81574d18fea64d8d5..e751e885617471366a97c16671d58558c1305872 100644 (file)
@@ -3,6 +3,7 @@
  *
  * Copyright (C) 2012 Petteri Aimonen <jpa@sr.mail.kapsi.fi>
  * Copyright (C) 2014 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/>.
  */
 
-/* The VCD input module has the following options:
- *
- * numchannels: Maximum number of channels to use. The channels are
- *              detected in the same order as they are listed
- *              in the $var sections of the VCD file.
- *
- * skip:        Allows skipping until given timestamp in the file.
- *              This can speed up analyzing of long captures.
- *            
- *              Value < 0: Skip until first timestamp listed in
- *              the file. (default)
+/*
+ * The VCD input module has the following options. See the options[]
+ * declaration near the bottom of the input module's source file.
  *
- *              Value = 0: Do not skip, instead generate samples
- *              beginning from timestamp 0.
+ * numchannels: Maximum number of sigrok channels to create. VCD signals
+ *   are detected in their order of declaration in the VCD file header,
+ *   and mapped to sigrok channels.
  *
- *              Value > 0: Start at the given timestamp.
+ * skip: Allows to skip data at the start of the input file. This can
+ *   speed up operation on long captures.
+ *   Value < 0: Skip until first timestamp that is listed in the file.
+ *     (This is the default behaviour.)
+ *   Value = 0: Do not skip, instead generate samples beginning from
+ *     timestamp 0.
+ *   Value > 0: Start at the given timestamp.
  *
- * downsample:  Divide the samplerate by the given factor.
- *              This can speed up analyzing of long captures.
+ * downsample: Divide the samplerate by the given factor. This can
+ *   speed up operation on long captures.
  *
- * compress:    Compress idle periods longer than this value.
- *              This can speed up analyzing of long captures.
- *              Default 0 = don't compress.
+ * compress: Trim idle periods which are longer than this value to span
+ *   only this many timescale ticks. This can speed up operation on long
+ *   captures (default 0, don't compress).
  *
  * Based on Verilog standard IEEE Std 1364-2001 Version C
  *
  * - $var with 'wire' and 'reg' types of scalar variables
  * - $timescale definition for samplerate
  * - multiple character variable identifiers
+ * - same identifer used for multiple signals (identical values)
+ * - vector variables (bit vectors)
+ * - integer variables (analog signals with 0 digits, passed as single
+ *   precision float number)
+ * - real variables (analog signals, passed on with single precision,
+ *   arbitrary digits value, not user adjustable)
+ * - nested $scope, results in prefixed sigrok channel names
  *
  * Most important unsupported features:
- * - vector variables (bit vectors etc.)
- * - analog, integer and real number variables
- * - $dumpvars initial value declaration
- * - $scope namespaces
- * - more than 64 channels
+ * - $dumpvars initial value declaration (is not an issue if generators
+ *   provide sample data for the #0 timestamp, otherwise session data
+ *   starts from zero values, and catches up when the signal changes its
+ *   state to a supported value)
+ *
+ * Implementor's note: This input module specifically does _not_ use
+ * glib routines where they would hurt performance. Lots of memory
+ * allocations increase execution time not by percents but by huge
+ * factors. This motivated this module's custom code for splitting
+ * words on text lines, and pooling previously allocated buffers.
+ *
+ * TODO (in arbitrary order)
+ * - Map VCD scopes to sigrok channel groups?
+ *   - Does libsigrok support nested channel groups? Or is this feature
+ *     exclusive to Pulseview?
+ * - Check VCD input to VCD output behaviour. Verify that export and
+ *   re-import results in identical data (well, VCD's constraints on
+ *   timescale values is known to result in differences).
+ * - Check the minimum timestamp delta in the input data set, suggest
+ *   the downsample=N option to users for reduced resource consumption.
+ *   Popular VCD file creation utilities love to specify insanely tiny
+ *   timescale values in the pico or even femto seconds range. Which
+ *   results in huge sample counts after import, and potentially even
+ *   terminates the application due to resource exhaustion. This issue
+ *   only will vanish when common libsigrok infrastructure no longer
+ *   depends on constant rate streams of samples at discrete points
+ *   in time. The current input module implementation has code in place
+ *   to gather timestamp statistics, but the most appropriate condition
+ *   when to notify users is yet to be found.
+ * - Cleanup the implementation.
+ *   - Consistent use of the glib API (where appropriate).
+ *   - More appropriate variable/function identifiers.
+ *   - More robust handling of multi-word input phrases and chunked
+ *     input buffers? This implementation assumes that e.g. b[01]+
+ *     patterns are complete when they start, and the signal identifier
+ *     is available as well. Which may be true assuming that input data
+ *     comes in complete text lines.
+ *   - See if other input modules have learned lessons that we could
+ *     benefit from here as well? Pointless BOM (done), line oriented
+ *     processing with EOL variants and with optional last EOL, module
+ *     state reset and file re-read (stable channels list), buffered
+ *     session feed, synchronized feed for mixed signal sources, digits
+ *     or formats support for analog input, single vs double precision,
+ *     etc.
+ *   - Re-consider logging. Verbosity levels should be acceptable,
+ *     but volume is an issue. Drop duplicates, and drop messages from
+ *     known good code paths.
  */
 
 #include <config.h>
-#include <stdlib.h>
+
 #include <glib.h>
-#include <stdio.h>
-#include <string.h>
 #include <libsigrok/libsigrok.h>
 #include "libsigrok-internal.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 
 #define LOG_PREFIX "input/vcd"
 
-#define DEFAULT_NUM_CHANNELS 8
-#define CHUNKSIZE (1024 * 1024)
+#define CHUNK_SIZE (4 * 1024 * 1024)
+#define SCOPE_SEP '.'
 
 struct context {
+       struct vcd_user_opt {
+               size_t maxchannels; /* sigrok channels (output) */
+               uint64_t downsample;
+               uint64_t compress;
+               uint64_t skip_starttime;
+               gboolean skip_specified;
+       } options;
+       gboolean use_skip;
        gboolean started;
        gboolean got_header;
+       uint64_t prev_timestamp;
        uint64_t samplerate;
-       unsigned int maxchannels;
-       unsigned int channelcount;
-       int downsample;
-       unsigned compress;
-       int64_t skip;
+       size_t vcdsignals; /* VCD signals (input) */
+       GSList *ignored_signals;
+       gboolean data_after_timestamp;
+       gboolean ignore_end_keyword;
        gboolean skip_until_end;
        GSList *channels;
-       size_t bytes_per_sample;
-       size_t samples_in_buffer;
-       uint8_t *buffer;
-       uint8_t *current_levels;
+       size_t unit_size;
+       size_t logic_count;
+       size_t analog_count;
+       uint8_t *current_logic;
+       float *current_floats;
+       struct {
+               size_t max_bits;
+               size_t unit_size;
+               uint8_t *value;
+               size_t sig_count;
+       } conv_bits;
+       GString *scope_prefix;
+       struct feed_queue_logic *feed_logic;
+       struct split_state {
+               size_t alloced;
+               char **words;
+               gboolean in_use;
+       } split;
+       struct ts_stats {
+               size_t total_ts_seen;
+               uint64_t last_ts_value;
+               uint64_t last_ts_delta;
+               size_t min_count;
+               struct {
+                       uint64_t delta;
+                       size_t count;
+               } min_items[2];
+               uint32_t early_check_shift;
+               size_t early_last_emitted;
+       } ts_stats;
+       struct vcd_prev {
+               GSList *sr_channels;
+               GSList *sr_groups;
+       } prev;
 };
 
 struct vcd_channel {
-       gchar *name;
-       gchar *identifier;
+       char *name;
+       char *identifier;
+       size_t size;
+       enum sr_channeltype type;
+       size_t array_index;
+       size_t byte_idx;
+       uint8_t bit_mask;
+       char *base_name;
+       size_t range_lower, range_upper;
+       int submit_digits;
+       struct feed_queue_analog *feed_analog;
 };
 
+static void free_channel(void *data)
+{
+       struct vcd_channel *vcd_ch;
+
+       vcd_ch = data;
+       if (!vcd_ch)
+               return;
+
+       g_free(vcd_ch->name);
+       g_free(vcd_ch->identifier);
+       g_free(vcd_ch->base_name);
+       feed_queue_analog_free(vcd_ch->feed_analog);
+
+       g_free(vcd_ch);
+}
+
+/* TODO Drop the local decl when this has become a common helper. */
+void sr_channel_group_free(struct sr_channel_group *cg);
+
+/* Wrapper for GDestroyNotify compatibility. */
+static void cg_free(void *p)
+{
+       sr_channel_group_free(p);
+}
+
+/*
+ * Another timestamp delta was observed, update statistics: Update the
+ * sorted list of minimum values, and increment the occurance counter.
+ * Returns the position of the item's statistics slot, or returns a huge
+ * invalid index when the current delta is larger than previously found
+ * values.
+ */
+static size_t ts_stats_update_min(struct ts_stats *stats, uint64_t delta)
+{
+       size_t idx, copy_idx;
+
+       /* Advance over previously recorded values which are smaller. */
+       idx = 0;
+       while (idx < stats->min_count && stats->min_items[idx].delta < delta)
+               idx++;
+       if (idx == ARRAY_SIZE(stats->min_items))
+               return idx;
+
+       /* Found the exact value that previously was registered? */
+       if (stats->min_items[idx].delta == delta) {
+               stats->min_items[idx].count++;
+               return idx;
+       }
+
+       /* Allocate another slot, bubble up larger values as needed. */
+       if (stats->min_count < ARRAY_SIZE(stats->min_items))
+               stats->min_count++;
+       for (copy_idx = stats->min_count - 1; copy_idx > idx; copy_idx--)
+               stats->min_items[copy_idx] = stats->min_items[copy_idx - 1];
+
+       /* Start tracking this value in the found or freed slot. */
+       memset(&stats->min_items[idx], 0, sizeof(stats->min_items[idx]));
+       stats->min_items[idx].delta = delta;
+       stats->min_items[idx].count++;
+
+       return idx;
+}
+
+/*
+ * Intermediate check for extreme oversampling in the input data. Rate
+ * limited emission of warnings to avoid noise, "late" emission of the
+ * first potential message to avoid false positives, yet need to  emit
+ * the messages early (*way* before EOF) to raise awareness.
+ *
+ * TODO
+ * Tune the limits, improve perception and usefulness of these checks.
+ * Need to start emitting messages soon enough to be seen by users. Yet
+ * avoid unnecessary messages for valid input's idle/quiet phases. Slow
+ * input transitions are perfectly legal before bursty phases are seen
+ * in the input data. Needs the check become an option, on by default,
+ * but suppressable by users?
+ */
+static void ts_stats_check_early(struct ts_stats *stats)
+{
+       static const struct {
+               uint64_t delta;
+               size_t count;
+       } *cp, check_points[] = {
+               {     100, 1000000, }, /* Still x100 after 1mio transitions. */
+               {    1000,  100000, }, /* Still x1k after 100k transitions. */
+               {   10000,   10000, }, /* Still x10k after 10k transitions. */
+               { 1000000,    2500, }, /* Still x1m after 2.5k transitions. */
+       };
+
+       size_t cp_idx;
+       uint64_t seen_delta, check_delta;
+       size_t seen_count;
+
+       /* Get the current minimum's value and count. */
+       if (!stats->min_count)
+               return;
+       seen_delta = stats->min_items[0].delta;
+       seen_count = stats->min_items[0].count;
+
+       /* Emit at most one weak message per import. */
+       if (stats->early_last_emitted)
+               return;
+
+       /* Check arbitrary marks, emit rate limited warnings. */
+       (void)seen_count;
+       check_delta = seen_delta >> stats->early_check_shift;
+       for (cp_idx = 0; cp_idx < ARRAY_SIZE(check_points); cp_idx++) {
+               cp = &check_points[cp_idx];
+               /* No other match can happen below. Done iterating. */
+               if (stats->total_ts_seen > cp->count)
+                       return;
+               /* Advance to the next checkpoint description. */
+               if (stats->total_ts_seen != cp->count)
+                       continue;
+               /* First occurance of that timestamp count. Check the value. */
+               sr_dbg("TS early chk: total %zu, min delta %" PRIu64 " / %" PRIu64 ".",
+                       cp->count, seen_delta, check_delta);
+               if (check_delta < cp->delta)
+                       return;
+               sr_warn("Low change rate? (weak estimate, min TS delta %" PRIu64 " after %zu timestamps)",
+                       seen_delta, stats->total_ts_seen);
+               sr_warn("Consider using the downsample=N option, or increasing its value.");
+               stats->early_last_emitted = stats->total_ts_seen;
+               return;
+       }
+}
+
+/* Reset the internal state of the timestamp tracker. */
+static int ts_stats_prep(struct context *inc)
+{
+       struct ts_stats *stats;
+       uint64_t down_sample_value;
+       uint32_t down_sample_shift;
+
+       stats = &inc->ts_stats;
+       memset(stats, 0, sizeof(*stats));
+
+       down_sample_value = inc->options.downsample;
+       down_sample_shift = 0;
+       while (down_sample_value >= 2) {
+               down_sample_shift++;
+               down_sample_value /= 2;
+       }
+       stats->early_check_shift = down_sample_shift;
+
+       return SR_OK;
+}
+
+/* Inspect another timestamp that was received. */
+static int ts_stats_check(struct ts_stats *stats, uint64_t curr_ts)
+{
+       uint64_t last_ts, delta;
+
+       last_ts = stats->last_ts_value;
+       stats->last_ts_value = curr_ts;
+       stats->total_ts_seen++;
+       if (stats->total_ts_seen < 2)
+               return SR_OK;
+
+       delta = curr_ts - last_ts;
+       stats->last_ts_delta = delta;
+       (void)ts_stats_update_min(stats, delta);
+
+       ts_stats_check_early(stats);
+
+       return SR_OK;
+}
+
+/* Postprocess internal timestamp tracker state. */
+static int ts_stats_post(struct context *inc, gboolean ignore_terminal)
+{
+       struct ts_stats *stats;
+       size_t min_idx;
+       uint64_t delta, over_sample, over_sample_scaled, suggest_factor;
+       enum sr_loglevel log_level;
+       gboolean is_suspicious, has_downsample;
+
+       stats = &inc->ts_stats;
+
+       /*
+        * Lookup the smallest timestamp delta which was found during
+        * data import. Ignore the last delta if its timestamp was never
+        * followed by data, and this was the only occurance. Absence of
+        * result data is non-fatal here -- this code exclusively serves
+        * to raise users' awareness of potential pitfalls, but does not
+        * change behaviour of data processing.
+        *
+        * TODO Also filter by occurance count? To not emit warnings when
+        * captured signals only change slowly by design. Only warn when
+        * the sample rate and samples count product exceeds a threshold?
+        * See below for the necessity (and potential) to adjust the log
+        * message's severity and content.
+        */
+       min_idx = 0;
+       if (ignore_terminal) do {
+               if (min_idx >= stats->min_count)
+                       break;
+               delta = stats->last_ts_delta;
+               if (stats->min_items[min_idx].delta != delta)
+                       break;
+               if (stats->min_items[min_idx].count != 1)
+                       break;
+               min_idx++;
+       } while (0);
+       if (min_idx >= stats->min_count)
+               return SR_OK;
+
+       /*
+        * TODO Refine the condition whether to notify the user, and
+        * which severity to use after having inspected all input data.
+        * Any detail could get involved which previously was gathered
+        * during data processing: total sample count, channel count
+        * including their data type and bits width, the oversampling
+        * factor (minimum observed "change rate"), or any combination
+        * thereof. The current check is rather simple (unconditional
+        * warning for ratios starting at 100, regardless of sample or
+        * channel count).
+        */
+       over_sample = stats->min_items[min_idx].delta;
+       over_sample_scaled = over_sample / inc->options.downsample;
+       sr_dbg("TS post stats: oversample unscaled %" PRIu64 ", scaled %" PRIu64,
+               over_sample, over_sample_scaled);
+       if (over_sample_scaled < 10) {
+               sr_dbg("TS post stats: Low oversampling ratio, good.");
+               return SR_OK;
+       }
+
+       /*
+        * Avoid constructing the message from several tiny pieces by
+        * design, because this would be hard on translators. Stick with
+        * complete sentences instead, and accept the redundancy in the
+        * user's interest.
+        */
+       log_level = (over_sample_scaled > 20) ? SR_LOG_WARN : SR_LOG_INFO;
+       is_suspicious = over_sample_scaled > 20;
+       if (is_suspicious) {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suspiciously low overall change rate (total min TS delta %" PRIu64 ").",
+                       over_sample_scaled);
+       } else {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Low overall change rate (total min TS delta %" PRIu64 ").",
+                       over_sample_scaled);
+       }
+       has_downsample = inc->options.downsample > 1;
+       suggest_factor = inc->options.downsample;
+       while (over_sample_scaled >= 10) {
+               suggest_factor *= 10;
+               over_sample_scaled /= 10;
+       }
+       if (has_downsample) {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suggest higher downsample value, like %" PRIu64 ".",
+                       suggest_factor);
+       } else {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suggest to downsample, value like %" PRIu64 ".",
+                       suggest_factor);
+       }
+
+       return SR_OK;
+}
+
+static void check_remove_bom(GString *buf)
+{
+       static const char *bom_text = "\xef\xbb\xbf";
+
+       if (buf->len < strlen(bom_text))
+               return;
+       if (strncmp(buf->str, bom_text, strlen(bom_text)) != 0)
+               return;
+       g_string_erase(buf, 0, strlen(bom_text));
+}
+
 /*
  * Reads a single VCD section from input file and parses it to name/contents.
- * e.g. $timescale 1ps $end  => "timescale" "1ps"
+ * e.g. $timescale 1ps $end => "timescale" "1ps"
  */
-static gboolean parse_section(GString *buf, gchar **name, gchar **contents)
+static gboolean parse_section(GString *buf, char **name, char **contents)
 {
-       GString *sname, *scontent;
+       static const char *end_text = "$end";
+
        gboolean status;
-       unsigned int pos;
+       size_t pos, len;
+       const char *grab_start, *grab_end;
+       GString *sname, *scontent;
 
+       /* Preset falsy return values. Gets updated below. */
        *name = *contents = NULL;
        status = FALSE;
-       pos = 0;
 
        /* Skip any initial white-space. */
+       pos = 0;
        while (pos < buf->len && g_ascii_isspace(buf->str[pos]))
                pos++;
 
@@ -114,432 +489,1523 @@ static gboolean parse_section(GString *buf, gchar **name, gchar **contents)
        if (buf->str[pos++] != '$')
                return FALSE;
 
-       sname = g_string_sized_new(32);
-       scontent = g_string_sized_new(128);
-
        /* Read the section tag. */
+       grab_start = &buf->str[pos];
        while (pos < buf->len && !g_ascii_isspace(buf->str[pos]))
-               g_string_append_c(sname, buf->str[pos++]);
+               pos++;
+       grab_end = &buf->str[pos];
+       sname = g_string_new_len(grab_start, grab_end - grab_start);
 
        /* Skip whitespace before content. */
        while (pos < buf->len && g_ascii_isspace(buf->str[pos]))
                pos++;
 
-       /* Read the content. */
-       while (pos < buf->len - 4 && strncmp(buf->str + pos, "$end", 4))
-               g_string_append_c(scontent, buf->str[pos++]);
-
-       if (sname->len && pos < buf->len - 4 && !strncmp(buf->str + pos, "$end", 4)) {
-               status = TRUE;
-               pos += 4;
+       /* Read the content up to the '$end' marker. */
+       scontent = g_string_sized_new(128);
+       grab_start = &buf->str[pos];
+       grab_end = g_strstr_len(grab_start, buf->len - pos, end_text);
+       if (grab_end) {
+               /* Advance 'pos' to after '$end' and more whitespace. */
+               pos = grab_end - buf->str;
+               pos += strlen(end_text);
                while (pos < buf->len && g_ascii_isspace(buf->str[pos]))
                        pos++;
+
+               /* Grab the (trimmed) content text. */
+               while (grab_end > grab_start && g_ascii_isspace(grab_end[-1]))
+                       grab_end--;
+               len = grab_end - grab_start;
+               g_string_append_len(scontent, grab_start, len);
+               if (sname->len)
+                       status = TRUE;
+
+               /* Consume the input text which just was taken. */
                g_string_erase(buf, 0, pos);
        }
 
+       /* Return section name and content if a section was seen. */
        *name = g_string_free(sname, !status);
        *contents = g_string_free(scontent, !status);
-       if (*contents)
-               g_strchomp(*contents);
 
        return status;
 }
 
-static void free_channel(void *data)
-{
-       struct vcd_channel *vcd_ch = data;
-       g_free(vcd_ch->name);
-       g_free(vcd_ch->identifier);
-       g_free(vcd_ch);
-}
+/*
+ * The glib routine which splits an input text into a list of words also
+ * "provides empty strings" which application code then needs to remove.
+ * And copies of the input text get allocated for all words.
+ *
+ * The repeated memory allocation is acceptable for small workloads like
+ * parsing the header sections. But the heavy lifting for sample data is
+ * done by DIY code to speedup execution. The use of glib routines would
+ * severely hurt throughput. Allocated memory gets re-used while a strict
+ * ping-pong pattern is assumed (each text line of input data enters and
+ * leaves in a strict symmetrical manner, due to the organization of the
+ * receive() routine and parse calls).
+ */
 
-/* Remove empty parts from an array returned by g_strsplit. */
+/* Remove empty parts from an array returned by g_strsplit(). */
 static void remove_empty_parts(gchar **parts)
 {
-       gchar **src = parts;
-       gchar **dest = parts;
-       while (*src != NULL) {
-               if (**src != '\0')
-                       *dest++ = *src;
+       gchar **src, **dest;
+
+       src = dest = parts;
+       while (*src) {
+               if (!**src) {
+                       g_free(*src);
+               } else {
+                       if (dest != src)
+                               *dest = *src;
+                       dest++;
+               }
                src++;
        }
-
        *dest = NULL;
 }
 
+static char **split_text_line(struct context *inc, char *text, size_t *count)
+{
+       struct split_state *state;
+       size_t counted, alloced, wanted;
+       char **words, *p, **new_words;
+
+       state = &inc->split;
+
+       if (count)
+               *count = 0;
+
+       if (state->in_use) {
+               sr_dbg("coding error, split() called while \"in use\".");
+               return NULL;
+       }
+
+       /*
+        * Seed allocation when invoked for the first time. Assume
+        * simple logic data, start with a few words per line. Will
+        * automatically adjust with subsequent use.
+        */
+       if (!state->alloced) {
+               alloced = 20;
+               words = g_malloc(sizeof(words[0]) * alloced);
+               if (!words)
+                       return NULL;
+               state->alloced = alloced;
+               state->words = words;
+       }
+
+       /* Start with most recently allocated word list space. */
+       alloced = state->alloced;
+       words = state->words;
+       counted = 0;
+
+       /* As long as more input text remains ... */
+       p = text;
+       while (*p) {
+               /* Resize word list if needed. Just double the size. */
+               if (counted + 1 >= alloced) {
+                       wanted = 2 * alloced;
+                       new_words = g_realloc(words, sizeof(words[0]) * wanted);
+                       if (!new_words) {
+                               return NULL;
+                       }
+                       words = new_words;
+                       alloced = wanted;
+                       state->words = words;
+                       state->alloced = alloced;
+               }
+
+               /* Skip leading spaces. */
+               while (g_ascii_isspace(*p))
+                       p++;
+               if (!*p)
+                       break;
+
+               /* Add found word to word list. */
+               words[counted++] = p;
+
+               /* Find end of the word. Terminate loop upon EOS. */
+               while (*p && !g_ascii_isspace(*p))
+                       p++;
+               if (!*p)
+                       break;
+
+               /* More text follows. Terminate the word. */
+               *p++ = '\0';
+       }
+
+       /*
+        * NULL terminate the word list. Provide its length so that
+        * calling code need not re-iterate the list to get the count.
+        */
+       words[counted] = NULL;
+       if (count)
+               *count = counted;
+       state->in_use = TRUE;
+
+       return words;
+}
+
+static void free_text_split(struct context *inc, char **words)
+{
+       struct split_state *state;
+
+       state = &inc->split;
+
+       if (words && words != state->words) {
+               sr_dbg("coding error, free() arg differs from split() result.");
+       }
+
+       /* "Double free" finally releases the memory. */
+       if (!state->in_use) {
+               g_free(state->words);
+               state->words = NULL;
+               state->alloced = 0;
+       }
+
+       /* Mark as no longer in use. */
+       state->in_use = FALSE;
+}
+
+static gboolean have_header(GString *buf)
+{
+       static const char *enddef_txt = "$enddefinitions";
+       static const char *end_txt = "$end";
+
+       char *p, *p_stop;
+
+       /* Search for "end of definitions" section keyword. */
+       p = g_strstr_len(buf->str, buf->len, enddef_txt);
+       if (!p)
+               return FALSE;
+       p += strlen(enddef_txt);
+
+       /* Search for end of section (content expected to be empty). */
+       p_stop = &buf->str[buf->len];
+       p_stop -= strlen(end_txt);
+       while (p < p_stop && g_ascii_isspace(*p))
+               p++;
+       if (strncmp(p, end_txt, strlen(end_txt)) != 0)
+               return FALSE;
+       p += strlen(end_txt);
+
+       return TRUE;
+}
+
+static int parse_timescale(struct context *inc, char *contents)
+{
+       uint64_t p, q;
+
+       /*
+        * The standard allows for values 1, 10 or 100
+        * and units s, ms, us, ns, ps and fs.
+        */
+       if (sr_parse_period(contents, &p, &q) != SR_OK) {
+               sr_err("Parsing $timescale failed.");
+               return SR_ERR_DATA;
+       }
+
+       inc->samplerate = q / p;
+       sr_dbg("Samplerate: %" PRIu64, inc->samplerate);
+       if (q % p != 0) {
+               /* Does not happen unless time value is non-standard */
+               sr_warn("Inexact rounding of samplerate, %" PRIu64 " / %" PRIu64 " to %" PRIu64 " Hz.",
+                       q, p, inc->samplerate);
+       }
+
+       return SR_OK;
+}
+
 /*
- * Parse VCD header to get values for context structure.
- * The context structure should be zeroed before calling this.
+ * Handle '$scope' and '$upscope' sections in the input file. Assume that
+ * input signals have a "base name", which may be ambiguous within the
+ * file. These names get declared within potentially nested scopes, which
+ * this implementation uses to create longer but hopefully unique and
+ * thus more usable sigrok channel names.
+ *
+ * Track the currently effective scopes in a string variable to simplify
+ * the channel name creation. Start from an empty string, then append the
+ * scope name and a separator when a new scope opens, and remove the last
+ * scope name when a scope closes. This allows to simply prefix basenames
+ * with the current scope to get a full name.
+ *
+ * It's an implementation detail to keep the trailing NUL here in the
+ * GString member, to simplify the g_strconcat() call in the channel name
+ * creation.
+ *
+ * TODO
+ * - Check whether scope types must get supported, this implementation
+ *   does not distinguish between 'module' and 'begin' and what else
+ *   may be seen. The first word simply gets ignored.
+ * - Check the allowed alphabet for scope names. This implementation
+ *   assumes "programming language identifier" style (alphanumeric with
+ *   underscores, plus brackets since we've seen them in example files).
  */
-static gboolean parse_header(const struct sr_input *in, GString *buf)
+static int parse_scope(struct context *inc, char *contents, gboolean is_up)
 {
+       char *sep_pos, *name_pos;
+       char **parts;
+       size_t length;
+
+       /*
+        * The 'upscope' case, drop one scope level (if available). Accept
+        * excess 'upscope' calls, assume that a previous 'scope' section
+        * was ignored because it referenced our software package's name.
+        */
+       if (is_up) {
+               /*
+                * Check for a second right-most separator (and position
+                * right behind that, which is the start of the last
+                * scope component), or fallback to the start of string.
+                * g_string_erase() from that positon to the end to drop
+                * the last component.
+                */
+               name_pos = inc->scope_prefix->str;
+               do {
+                       sep_pos = strrchr(name_pos, SCOPE_SEP);
+                       if (!sep_pos)
+                               break;
+                       *sep_pos = '\0';
+                       sep_pos = strrchr(name_pos, SCOPE_SEP);
+                       if (!sep_pos)
+                               break;
+                       name_pos = ++sep_pos;
+               } while (0);
+               length = name_pos - inc->scope_prefix->str;
+               g_string_truncate(inc->scope_prefix, length);
+               g_string_append_c(inc->scope_prefix, '\0');
+               sr_dbg("$upscope, prefix now: \"%s\"", inc->scope_prefix->str);
+               return SR_OK;
+       }
+
+       /*
+        * The 'scope' case, add another scope level. But skip our own
+        * package name, assuming that this is an artificial node which
+        * was emitted by libsigrok's VCD output module.
+        */
+       sr_spew("$scope, got: \"%s\"", contents);
+       parts = g_strsplit_set(contents, " \r\n\t", 0);
+       remove_empty_parts(parts);
+       length = g_strv_length(parts);
+       if (length != 2) {
+               sr_err("Unsupported 'scope' syntax: %s", contents);
+               g_strfreev(parts);
+               return SR_ERR_DATA;
+       }
+       name_pos = parts[1];
+       if (strcmp(name_pos, PACKAGE_NAME) == 0) {
+               sr_info("Skipping scope with application's package name: %s",
+                       name_pos);
+               *name_pos = '\0';
+       }
+       if (*name_pos) {
+               /* Drop NUL, append scope name and separator, and re-add NUL. */
+               g_string_truncate(inc->scope_prefix, inc->scope_prefix->len - 1);
+               g_string_append_printf(inc->scope_prefix,
+                       "%s%c%c", name_pos, SCOPE_SEP, '\0');
+       }
+       g_strfreev(parts);
+       sr_dbg("$scope, prefix now: \"%s\"", inc->scope_prefix->str);
+
+       return SR_OK;
+}
+
+/**
+ * Parse a $var section which describes a VCD signal ("variable").
+ *
+ * @param[in] inc Input module context.
+ * @param[in] contents Input text, content of $var section.
+ */
+static int parse_header_var(struct context *inc, char *contents)
+{
+       char **parts;
+       size_t length;
+       char *type, *size_txt, *id, *ref, *idx;
+       gboolean is_reg, is_wire, is_real, is_int;
+       enum sr_channeltype ch_type;
+       size_t size, next_size;
        struct vcd_channel *vcd_ch;
-       uint64_t p, q;
+
+       /*
+        * Format of $var or $reg header specs:
+        * $var type size identifier reference [opt-index] $end
+        */
+       parts = g_strsplit_set(contents, " \r\n\t", 0);
+       remove_empty_parts(parts);
+       length = g_strv_length(parts);
+       if (length != 4 && length != 5) {
+               sr_warn("$var section should have 4 or 5 items");
+               g_strfreev(parts);
+               return SR_ERR_DATA;
+       }
+
+       type = parts[0];
+       size_txt = parts[1];
+       id = parts[2];
+       ref = parts[3];
+       idx = parts[4];
+       if (idx && !*idx)
+               idx = NULL;
+       is_reg = g_strcmp0(type, "reg") == 0;
+       is_wire = g_strcmp0(type, "wire") == 0;
+       is_real = g_strcmp0(type, "real") == 0;
+       is_int = g_strcmp0(type, "integer") == 0;
+
+       if (is_reg || is_wire) {
+               ch_type = SR_CHANNEL_LOGIC;
+       } else if (is_real || is_int) {
+               ch_type = SR_CHANNEL_ANALOG;
+       } else {
+               sr_info("Unsupported signal type: '%s'", type);
+               g_strfreev(parts);
+               return SR_ERR_DATA;
+       }
+
+       size = strtol(size_txt, NULL, 10);
+       if (ch_type == SR_CHANNEL_ANALOG) {
+               if (is_real && size != 32 && size != 64) {
+                       /*
+                        * The VCD input module does not depend on the
+                        * specific width of the floating point value.
+                        * This is just for information. Upon value
+                        * changes, a mere string gets converted to
+                        * float, so we may not care at all.
+                        *
+                        * Strictly speaking we might warn for 64bit
+                        * (double precision) declarations, because
+                        * sigrok internally uses single precision
+                        * (32bit) only.
+                        */
+                       sr_info("Unexpected real width: '%s'", size_txt);
+               }
+               /* Simplify code paths below, by assuming size 1. */
+               size = 1;
+       }
+       if (!size) {
+               sr_warn("Unsupported signal size: '%s'", size_txt);
+               g_strfreev(parts);
+               return SR_ERR_DATA;
+       }
+       if (inc->conv_bits.max_bits < size)
+               inc->conv_bits.max_bits = size;
+       next_size = inc->logic_count + inc->analog_count + size;
+       if (inc->options.maxchannels && next_size > inc->options.maxchannels) {
+               sr_warn("Skipping '%s%s', exceeds requested channel count %zu.",
+                       ref, idx ? idx : "", inc->options.maxchannels);
+               inc->ignored_signals = g_slist_append(inc->ignored_signals,
+                       g_strdup(id));
+               g_strfreev(parts);
+               return SR_OK;
+       }
+
+       vcd_ch = g_malloc0(sizeof(*vcd_ch));
+       vcd_ch->identifier = g_strdup(id);
+       vcd_ch->name = g_strconcat(inc->scope_prefix->str, ref, idx, NULL);
+       vcd_ch->size = size;
+       vcd_ch->type = ch_type;
+       switch (ch_type) {
+       case SR_CHANNEL_LOGIC:
+               vcd_ch->array_index = inc->logic_count;
+               vcd_ch->byte_idx = vcd_ch->array_index / 8;
+               vcd_ch->bit_mask = 1 << (vcd_ch->array_index % 8);
+               inc->logic_count += size;
+               break;
+       case SR_CHANNEL_ANALOG:
+               vcd_ch->array_index = inc->analog_count++;
+               /* TODO: Use proper 'digits' value for this input module. */
+               vcd_ch->submit_digits = is_real ? 2 : 0;
+               break;
+       }
+       inc->vcdsignals++;
+       sr_spew("VCD signal %zu '%s' ID '%s' (size %zu), sr type %s, idx %zu.",
+               inc->vcdsignals, vcd_ch->name,
+               vcd_ch->identifier, vcd_ch->size,
+               vcd_ch->type == SR_CHANNEL_ANALOG ? "A" : "L",
+               vcd_ch->array_index);
+       inc->channels = g_slist_append(inc->channels, vcd_ch);
+       g_strfreev(parts);
+
+       return SR_OK;
+}
+
+/**
+ * Construct the name of the nth sigrok channel for a VCD signal.
+ *
+ * Uses the VCD signal name for scalar types and single-bit signals.
+ * Uses "signal.idx" for multi-bit VCD signals without a range spec in
+ * their declaration. Uses "signal[idx]" when a range is known and was
+ * verified.
+ *
+ * @param[in] vcd_ch The VCD signal's description.
+ * @param[in] idx The sigrok channel's index within the VCD signal's group.
+ *
+ * @return An allocated text buffer which callers need to release, #NULL
+ *   upon failure to create a sigrok channel name.
+ */
+static char *get_channel_name(struct vcd_channel *vcd_ch, size_t idx)
+{
+       char *open_pos, *close_pos, *check_pos, *endptr;
+       gboolean has_brackets, has_range;
+       size_t upper, lower, tmp;
+       char *ch_name;
+
+       /* Handle simple scalar types, and single-bit logic first. */
+       if (vcd_ch->size <= 1)
+               return g_strdup(vcd_ch->name);
+
+       /*
+        * If not done before: Search for a matching pair of brackets in
+        * the right-most position at the very end of the string. Get the
+        * two colon separated numbers between the brackets, which are
+        * the range limits for array indices into the multi-bit signal.
+        * Grab the "base name" of the VCD signal.
+        *
+        * Notice that arrays can get nested. Earlier path components can
+        * be indexed as well, that's why we need the right-most range.
+        * This implementation does not handle bit vectors of size 1 here
+        * by explicit logic. The check for a [0:0] range would even fail.
+        * But the case of size 1 is handled above, and "happens to" give
+        * the expected result (just the VCD signal name).
+        *
+        * This implementation also deals with range limits in the reverse
+        * order, as well as ranges which are not 0-based (like "[4:7]").
+        */
+       if (!vcd_ch->base_name) {
+               has_range = TRUE;
+               open_pos = strrchr(vcd_ch->name, '[');
+               close_pos = strrchr(vcd_ch->name, ']');
+               if (close_pos && close_pos[1])
+                       close_pos = NULL;
+               has_brackets = open_pos && close_pos && close_pos > open_pos;
+               if (!has_brackets)
+                       has_range = FALSE;
+               if (has_range) {
+                       check_pos = &open_pos[1];
+                       endptr = NULL;
+                       upper = strtoul(check_pos, &endptr, 10);
+                       if (!endptr || *endptr != ':')
+                               has_range = FALSE;
+               }
+               if (has_range) {
+                       check_pos = &endptr[1];
+                       endptr = NULL;
+                       lower = strtoul(check_pos, &endptr, 10);
+                       if (!endptr || endptr != close_pos)
+                               has_range = FALSE;
+               }
+               if (has_range && lower > upper) {
+                       tmp = lower;
+                       lower = upper;
+                       upper = tmp;
+               }
+               if (has_range) {
+                       if (lower >= upper)
+                               has_range = FALSE;
+                       if (upper + 1 - lower != vcd_ch->size)
+                               has_range = FALSE;
+               }
+               if (has_range) {
+                       /* Temporarily patch the VCD channel's name. */
+                       *open_pos = '\0';
+                       vcd_ch->base_name = g_strdup(vcd_ch->name);
+                       *open_pos = '[';
+                       vcd_ch->range_lower = lower;
+                       vcd_ch->range_upper = upper;
+               }
+       }
+       has_range = vcd_ch->range_lower + vcd_ch->range_upper;
+       if (has_range && idx >= vcd_ch->size)
+               has_range = FALSE;
+       if (!has_range)
+               return g_strdup_printf("%s.%zu", vcd_ch->name, idx);
+
+       /*
+        * Create a sigrok channel name with just the bit's index in
+        * brackets. This avoids "name[7:0].3" results, instead results
+        * in "name[3]".
+        */
+       ch_name = g_strdup_printf("%s[%zu]",
+               vcd_ch->base_name, vcd_ch->range_lower + idx);
+       return ch_name;
+}
+
+/*
+ * Create (analog or logic) sigrok channels for the VCD signals. Create
+ * multiple sigrok channels for vector input since sigrok has no concept
+ * of multi-bit signals. Create a channel group for the vector's bits
+ * though to reflect that they form a unit. This is beneficial when UIs
+ * support optional "collapsed" displays of channel groups (like
+ * "parallel bus, hex output").
+ *
+ * Defer channel creation until after completion of parsing the input
+ * file header. Make sure to create all logic channels first before the
+ * analog channels get created. This avoids issues with the mapping of
+ * channel indices to bitmap positions in the sample buffer.
+ */
+static void create_channels(const struct sr_input *in,
+       struct sr_dev_inst *sdi, enum sr_channeltype ch_type)
+{
+       struct context *inc;
+       size_t ch_idx;
+       GSList *l;
+       struct vcd_channel *vcd_ch;
+       size_t size_idx;
+       char *ch_name;
+       struct sr_channel_group *cg;
+       struct sr_channel *ch;
+
+       inc = in->priv;
+
+       ch_idx = 0;
+       if (ch_type > SR_CHANNEL_LOGIC)
+               ch_idx += inc->logic_count;
+       if (ch_type > SR_CHANNEL_ANALOG)
+               ch_idx += inc->analog_count;
+       for (l = inc->channels; l; l = l->next) {
+               vcd_ch = l->data;
+               if (vcd_ch->type != ch_type)
+                       continue;
+               cg = NULL;
+               if (vcd_ch->size != 1) {
+                       cg = g_malloc0(sizeof(*cg));
+                       cg->name = g_strdup(vcd_ch->name);
+               }
+               for (size_idx = 0; size_idx < vcd_ch->size; size_idx++) {
+                       ch_name = get_channel_name(vcd_ch, size_idx);
+                       sr_dbg("sigrok channel idx %zu, name %s, type %s, en %d.",
+                               ch_idx, ch_name,
+                               ch_type == SR_CHANNEL_ANALOG ? "A" : "L", TRUE);
+                       ch = sr_channel_new(sdi, ch_idx, ch_type, TRUE, ch_name);
+                       g_free(ch_name);
+                       ch_idx++;
+                       if (cg)
+                               cg->channels = g_slist_append(cg->channels, ch);
+               }
+               if (cg)
+                       sdi->channel_groups = g_slist_append(sdi->channel_groups, cg);
+       }
+}
+
+static void create_feeds(const struct sr_input *in)
+{
+       struct context *inc;
+       GSList *l;
+       struct vcd_channel *vcd_ch;
+       size_t ch_idx;
+       struct sr_channel *ch;
+
+       inc = in->priv;
+
+       /* Create one feed for logic data. */
+       if (inc->logic_count) {
+               inc->unit_size = (inc->logic_count + 7) / 8;
+               inc->feed_logic = feed_queue_logic_alloc(in->sdi,
+                       CHUNK_SIZE / inc->unit_size, inc->unit_size);
+       }
+
+       /* Create one feed per analog channel. */
+       for (l = inc->channels; l; l = l->next) {
+               vcd_ch = l->data;
+               if (vcd_ch->type != SR_CHANNEL_ANALOG)
+                       continue;
+               ch_idx = vcd_ch->array_index;
+               ch_idx += inc->logic_count;
+               ch = g_slist_nth_data(in->sdi->channels, ch_idx);
+               vcd_ch->feed_analog = feed_queue_analog_alloc(in->sdi,
+                       CHUNK_SIZE / sizeof(float),
+                       vcd_ch->submit_digits, ch);
+       }
+}
+
+/*
+ * Keep track of a previously created channel list, in preparation of
+ * re-reading the input file. Gets called from reset()/cleanup() paths.
+ */
+static void keep_header_for_reread(const struct sr_input *in)
+{
+       struct context *inc;
+
+       inc = in->priv;
+
+       g_slist_free_full(inc->prev.sr_groups, cg_free);
+       inc->prev.sr_groups = in->sdi->channel_groups;
+       in->sdi->channel_groups = NULL;
+
+       g_slist_free_full(inc->prev.sr_channels, sr_channel_free_cb);
+       inc->prev.sr_channels = in->sdi->channels;
+       in->sdi->channels = NULL;
+}
+
+/*
+ * Check whether the input file is being re-read, and refuse operation
+ * when essential parameters of the acquisition have changed in ways
+ * that are unexpected to calling applications. Gets called after the
+ * file header got parsed (again).
+ *
+ * Changing the channel list across re-imports of the same file is not
+ * supported, by design and for valid reasons, see bug #1215 for details.
+ * Users are expected to start new sessions when they change these
+ * essential parameters in the acquisition's setup. When we accept the
+ * re-read file, then make sure to keep using the previous channel list,
+ * applications may still reference them.
+ */
+static gboolean check_header_in_reread(const struct sr_input *in)
+{
+       struct context *inc;
+
+       if (!in)
+               return FALSE;
+       inc = in->priv;
+       if (!inc)
+               return FALSE;
+       if (!inc->prev.sr_channels)
+               return TRUE;
+
+       if (sr_channel_lists_differ(inc->prev.sr_channels, in->sdi->channels)) {
+               sr_err("Channel list change not supported for file re-read.");
+               return FALSE;
+       }
+
+       g_slist_free_full(in->sdi->channel_groups, cg_free);
+       in->sdi->channel_groups = inc->prev.sr_groups;
+       inc->prev.sr_groups = NULL;
+
+       g_slist_free_full(in->sdi->channels, sr_channel_free_cb);
+       in->sdi->channels = inc->prev.sr_channels;
+       inc->prev.sr_channels = NULL;
+
+       return TRUE;
+}
+
+/* Parse VCD file header sections (rate and variables declarations). */
+static int parse_header(const struct sr_input *in, GString *buf)
+{
        struct context *inc;
        gboolean status;
-       gchar *name, *contents, **parts;
+       char *name, *contents;
+       size_t size;
+       int ret;
 
        inc = in->priv;
-       name = contents = NULL;
+
+       /* Parse sections until complete header was seen. */
        status = FALSE;
+       name = contents = NULL;
+       inc->conv_bits.max_bits = 1;
        while (parse_section(buf, &name, &contents)) {
                sr_dbg("Section '%s', contents '%s'.", name, contents);
 
                if (g_strcmp0(name, "enddefinitions") == 0) {
                        status = TRUE;
-                       break;
-               } else if (g_strcmp0(name, "timescale") == 0) {
-                       /*
-                        * The standard allows for values 1, 10 or 100
-                        * and units s, ms, us, ns, ps and fs.
-                        */
-                       if (sr_parse_period(contents, &p, &q) == SR_OK) {
-                               inc->samplerate = q / p;
-                               if (q % p != 0) {
-                                       /* Does not happen unless time value is non-standard */
-                                       sr_warn("Inexact rounding of samplerate, %" PRIu64 " / %" PRIu64 " to %" PRIu64 " Hz.",
-                                               q, p, inc->samplerate);
-                               }
-
-                               sr_dbg("Samplerate: %" PRIu64, inc->samplerate);
-                       } else {
-                               sr_err("Parsing timescale failed.");
-                       }
-               } else if (g_strcmp0(name, "var") == 0) {
-                       /* Format: $var type size identifier reference [opt. index] $end */
-                       unsigned int length;
-
-                       parts = g_strsplit_set(contents, " \r\n\t", 0);
-                       remove_empty_parts(parts);
-                       length = g_strv_length(parts);
-
-                       if (length != 4 && length != 5)
-                               sr_warn("$var section should have 4 or 5 items");
-                       else if (g_strcmp0(parts[0], "reg") != 0 && g_strcmp0(parts[0], "wire") != 0)
-                               sr_info("Unsupported signal type: '%s'", parts[0]);
-                       else if (strtol(parts[1], NULL, 10) != 1)
-                               sr_info("Unsupported signal size: '%s'", parts[1]);
-                       else if (inc->channelcount >= inc->maxchannels)
-                               sr_warn("Skipping '%s%s' because only %d channels requested.",
-                                       parts[3], parts[4] ? : "", inc->maxchannels);
-                       else {
-                               vcd_ch = g_malloc(sizeof(struct vcd_channel));
-                               vcd_ch->identifier = g_strdup(parts[2]);
-                               if (length == 4)
-                                       vcd_ch->name = g_strdup(parts[3]);
-                               else
-                                       vcd_ch->name = g_strconcat(parts[3], parts[4], NULL);
-
-                               sr_info("Channel %d is '%s' identified by '%s'.",
-                                               inc->channelcount, vcd_ch->name, vcd_ch->identifier);
-
-                               inc->channels = g_slist_append(inc->channels, vcd_ch);
-                               inc->channelcount++;
-                       }
-
-                       g_strfreev(parts);
+                       goto done_section;
+               }
+               if (g_strcmp0(name, "timescale") == 0) {
+                       if (parse_timescale(inc, contents) != SR_OK)
+                               status = FALSE;
+                       goto done_section;
+               }
+               if (g_strcmp0(name, "scope") == 0) {
+                       if (parse_scope(inc, contents, FALSE) != SR_OK)
+                               status = FALSE;
+                       goto done_section;
+               }
+               if (g_strcmp0(name, "upscope") == 0) {
+                       if (parse_scope(inc, NULL, TRUE) != SR_OK)
+                               status = FALSE;
+                       goto done_section;
+               }
+               if (g_strcmp0(name, "var") == 0) {
+                       if (parse_header_var(inc, contents) != SR_OK)
+                               status = FALSE;
+                       goto done_section;
                }
 
+done_section:
                g_free(name);
                name = NULL;
                g_free(contents);
                contents = NULL;
+
+               if (status)
+                       break;
        }
        g_free(name);
        g_free(contents);
 
-       /*
-        * Compute how many bytes each sample will have and initialize the
-        * current levels. The current levels will be updated whenever VCD
-        * has changes.
-        */
-       inc->bytes_per_sample = (inc->channelcount + 7) / 8;
-       inc->current_levels = g_malloc0(inc->bytes_per_sample);
-
        inc->got_header = status;
+       if (!status)
+               return SR_ERR_DATA;
 
-       return status;
-}
-
-static int format_match(GHashTable *metadata)
-{
-       GString *buf, *tmpbuf;
-       gboolean status;
-       gchar *name, *contents;
-
-       buf = g_hash_table_lookup(metadata, GINT_TO_POINTER(SR_INPUT_META_HEADER));
-       tmpbuf = g_string_new_len(buf->str, buf->len);
+       /* Create sigrok channels here, late, logic before analog. */
+       create_channels(in, in->sdi, SR_CHANNEL_LOGIC);
+       create_channels(in, in->sdi, SR_CHANNEL_ANALOG);
+       if (!check_header_in_reread(in))
+               return SR_ERR_DATA;
+       create_feeds(in);
 
        /*
-        * If we can parse the first section correctly,
-        * then it is assumed to be a VCD file.
+        * Allocate space for text to number conversion, and buffers to
+        * hold current sample values before submission to the session
+        * feed. Allocate one buffer for all logic bits, and another for
+        * all floating point values of all analog channels.
+        *
+        * The buffers get updated when the VCD input stream communicates
+        * value changes. Upon reception of VCD timestamps, the buffer can
+        * provide the previously received values, to "fill in the gaps"
+        * in the generation of a continuous stream of samples for the
+        * sigrok session.
         */
-       status = parse_section(tmpbuf, &name, &contents);
-       g_string_free(tmpbuf, TRUE);
-       g_free(name);
-       g_free(contents);
+       size = (inc->conv_bits.max_bits + 7) / 8;
+       inc->conv_bits.unit_size = size;
+       inc->conv_bits.value = g_malloc0(size);
+       if (!inc->conv_bits.value)
+               return SR_ERR_MALLOC;
+
+       size = (inc->logic_count + 7) / 8;
+       inc->unit_size = size;
+       inc->current_logic = g_malloc0(size);
+       if (inc->unit_size && !inc->current_logic)
+               return SR_ERR_MALLOC;
+       size = sizeof(inc->current_floats[0]) * inc->analog_count;
+       inc->current_floats = g_malloc0(size);
+       if (size && !inc->current_floats)
+               return SR_ERR_MALLOC;
+       for (size = 0; size < inc->analog_count; size++)
+               inc->current_floats[size] = 0.;
+
+       ret = ts_stats_prep(inc);
+       if (ret != SR_OK)
+               return ret;
 
-       return status ? SR_OK : SR_ERR;
+       return SR_OK;
 }
 
-/* Send all accumulated bytes from inc->buffer. */
-static void send_buffer(const struct sr_input *in)
+/*
+ * Add N copies of previously received values to the session, before
+ * subsequent value changes will update the data buffer. Locally buffer
+ * sample data to minimize the number of send() calls.
+ */
+static void add_samples(const struct sr_input *in, size_t count, gboolean flush)
 {
        struct context *inc;
-       struct sr_datafeed_packet packet;
-       struct sr_datafeed_logic logic;
+       GSList *ch_list;
+       struct vcd_channel *vcd_ch;
+       struct feed_queue_analog *q;
+       float value;
 
        inc = in->priv;
 
-       if (inc->samples_in_buffer == 0)
-               return;
+       if (inc->logic_count) {
+               feed_queue_logic_submit(inc->feed_logic,
+                       inc->current_logic, count);
+               if (flush)
+                       feed_queue_logic_flush(inc->feed_logic);
+       }
+       for (ch_list = inc->channels; ch_list; ch_list = ch_list->next) {
+               vcd_ch = ch_list->data;
+               if (vcd_ch->type != SR_CHANNEL_ANALOG)
+                       continue;
+               q = vcd_ch->feed_analog;
+               if (!q)
+                       continue;
+               value = inc->current_floats[vcd_ch->array_index];
+               feed_queue_analog_submit(q, value, count);
+               if (flush)
+                       feed_queue_analog_flush(q);
+       }
+}
 
-       packet.type = SR_DF_LOGIC;
-       packet.payload = &logic;
-       logic.unitsize = inc->bytes_per_sample;
-       logic.data = inc->buffer;
-       logic.length = inc->bytes_per_sample * inc->samples_in_buffer;
-       sr_session_send(in->sdi, &packet);
-       inc->samples_in_buffer = 0;
+static gint vcd_compare_id(gconstpointer a, gconstpointer b)
+{
+       return strcmp((const char *)a, (const char *)b);
 }
 
-/*
- * Add N copies of the current sample to buffer.
- * When the buffer fills up, automatically send it.
- */
-static void add_samples(const struct sr_input *in, size_t count)
+static gboolean is_ignored(struct context *inc, const char *id)
 {
-       struct context *inc;
-       size_t samples_per_chunk;
-       size_t space_left, i;
-       uint8_t *p;
+       GSList *ignored;
 
-       inc = in->priv;
-       samples_per_chunk = CHUNKSIZE / inc->bytes_per_sample;
+       ignored = g_slist_find_custom(inc->ignored_signals, id, vcd_compare_id);
+       return ignored != NULL;
+}
 
-       while (count) {
-               space_left = samples_per_chunk - inc->samples_in_buffer;
+/*
+ * Get an analog channel's value from a bit pattern (VCD 'integer' type).
+ * The implementation assumes a maximum integer width (64bit), the API
+ * doesn't (beyond the return data type). The use of SR_CHANNEL_ANALOG
+ * channels may further constraint the number of significant digits
+ * (current asumption: float -> 23bit).
+ */
+static float get_int_val(uint8_t *in_bits_data, size_t in_bits_count)
+{
+       uint64_t int_value;
+       size_t byte_count, byte_idx;
+       float flt_value; /* typeof(inc->current_floats[0]) */
+
+       /* Convert bit pattern to integer number (limited range). */
+       int_value = 0;
+       byte_count = (in_bits_count + 7) / 8;
+       for (byte_idx = 0; byte_idx < byte_count; byte_idx++) {
+               if (byte_idx >= sizeof(int_value))
+                       break;
+               int_value |= *in_bits_data++ << (byte_idx * 8);
+       }
+       flt_value = int_value;
 
-               if (space_left > count)
-                       space_left = count;
+       return flt_value;
+}
 
-               p = inc->buffer + inc->samples_in_buffer * inc->bytes_per_sample;
-               for (i = 0; i < space_left; i++) {
-                       memcpy(p, inc->current_levels, inc->bytes_per_sample);
-                       p += inc->bytes_per_sample;
-                       inc->samples_in_buffer++;
-                       count--;
+/*
+ * Set a logic channel's level depending on the VCD signal's identifier
+ * and parsed value. Multi-bit VCD values will affect several sigrok
+ * channels. One VCD signal name can translate to several sigrok channels.
+ */
+static void process_bits(struct context *inc, char *identifier,
+       uint8_t *in_bits_data, size_t in_bits_count)
+{
+       size_t size;
+       gboolean have_int;
+       GSList *l;
+       struct vcd_channel *vcd_ch;
+       float int_val;
+       size_t bit_idx;
+       uint8_t *in_bit_ptr, in_bit_mask;
+       uint8_t *out_bit_ptr, out_bit_mask;
+       uint8_t bit_val;
+
+       size = 0;
+       have_int = FALSE;
+       int_val = 0;
+       for (l = inc->channels; l; l = l->next) {
+               vcd_ch = l->data;
+               if (g_strcmp0(identifier, vcd_ch->identifier) != 0)
+                       continue;
+               if (vcd_ch->type == SR_CHANNEL_ANALOG) {
+                       /* Special case for 'integer' VCD signal types. */
+                       size = vcd_ch->size; /* Flag for "VCD signal found". */
+                       if (!have_int) {
+                               int_val = get_int_val(in_bits_data, in_bits_count);
+                               have_int = TRUE;
+                       }
+                       inc->current_floats[vcd_ch->array_index] = int_val;
+                       continue;
+               }
+               if (vcd_ch->type != SR_CHANNEL_LOGIC)
+                       continue;
+               sr_spew("Processing %s data, id '%s', ch %zu sz %zu",
+                       (size == 1) ? "bit" : "vector",
+                       identifier, vcd_ch->array_index, vcd_ch->size);
+
+               /* Found our (logic) channel. Setup in/out bit positions. */
+               size = vcd_ch->size;
+               in_bit_ptr = in_bits_data;
+               in_bit_mask = 1 << 0;
+               out_bit_ptr = &inc->current_logic[vcd_ch->byte_idx];
+               out_bit_mask = vcd_ch->bit_mask;
+
+               /*
+                * Pass VCD input bit(s) to sigrok logic bits. Conversion
+                * must be done repeatedly because one VCD signal name
+                * can translate to several sigrok channels, and shifting
+                * a previously computed bit field to another channel's
+                * position in the buffer would be nearly as expensive,
+                * and certain would increase complexity of the code.
+                */
+               for (bit_idx = 0; bit_idx < size; bit_idx++) {
+                       /* Get the bit value from input data. */
+                       bit_val = 0;
+                       if (bit_idx < in_bits_count) {
+                               bit_val = *in_bit_ptr & in_bit_mask;
+                               in_bit_mask <<= 1;
+                               if (!in_bit_mask) {
+                                       in_bit_mask = 1 << 0;
+                                       in_bit_ptr++;
+                               }
+                       }
+                       /* Manipulate the sample buffer data image. */
+                       if (bit_val)
+                               *out_bit_ptr |= out_bit_mask;
+                       else
+                               *out_bit_ptr &= ~out_bit_mask;
+                       /* Update output position after bitmap update. */
+                       out_bit_mask <<= 1;
+                       if (!out_bit_mask) {
+                               out_bit_mask = 1 << 0;
+                               out_bit_ptr++;
+                       }
                }
+       }
+       if (!size && !is_ignored(inc, identifier))
+               sr_warn("VCD signal not found for ID '%s'.", identifier);
+}
 
-               if (inc->samples_in_buffer == samples_per_chunk)
-                       send_buffer(in);
+/*
+ * Set an analog channel's value from a floating point number. One
+ * VCD signal name can translate to several sigrok channels.
+ */
+static void process_real(struct context *inc, char *identifier, float real_val)
+{
+       gboolean found;
+       GSList *l;
+       struct vcd_channel *vcd_ch;
+
+       found = FALSE;
+       for (l = inc->channels; l; l = l->next) {
+               vcd_ch = l->data;
+               if (vcd_ch->type != SR_CHANNEL_ANALOG)
+                       continue;
+               if (g_strcmp0(identifier, vcd_ch->identifier) != 0)
+                       continue;
+
+               /* Found our (analog) channel. */
+               found = TRUE;
+               sr_spew("Processing real data, id '%s', ch %zu, val %.16g",
+                       identifier, vcd_ch->array_index, real_val);
+               inc->current_floats[vcd_ch->array_index] = real_val;
        }
+       if (!found && !is_ignored(inc, identifier))
+               sr_warn("VCD signal not found for ID '%s'.", identifier);
 }
 
-/* Parse a set of lines from the data section. */
-static void parse_contents(const struct sr_input *in, char *data)
+/*
+ * Converts a bit position's text character to a number value.
+ *
+ * TODO Check for complete coverage of Verilog's standard logic values
+ * (IEEE-1364). The set is said to be “01XZHUWL-”, which only a part of
+ * is handled here. What would be the complete mapping?
+ * - 0/L -> bit value 0
+ * - 1/H -> bit value 1
+ * - X "don't care" -> TODO
+ * - Z "high impedance" -> TODO
+ * - W "weak(?)" -> TODO
+ * - U "undefined" -> TODO
+ * - '-' "TODO" -> TODO
+ *
+ * For simplicity, this input module implementation maps "known low"
+ * values to 0, and "known high" values to 1. All other values will
+ * end up assuming "low" (return number 0), while callers might warn.
+ * It's up to users to provide compatible input data, or accept the
+ * warnings. Silently accepting unknown input data is not desirable.
+ */
+static uint8_t vcd_char_to_value(char bit_char, int *warn)
+{
+
+       bit_char = g_ascii_tolower(bit_char);
+
+       /* Convert the "undisputed" variants. */
+       if (bit_char == '0' || bit_char == 'l')
+               return 0;
+       if (bit_char == '1' || bit_char == 'h')
+               return 1;
+
+       /* Convert the "uncertain" variants. */
+       if (warn)
+               *warn = 1;
+       if (bit_char == 'x' || bit_char == 'z')
+               return 0;
+       if (bit_char == 'u')
+               return 0;
+       if (bit_char == '-')
+               return 0;
+
+       /* Unhandled input text. */
+       return ~0;
+}
+
+/* Parse one text line of the data section. */
+static int parse_textline(const struct sr_input *in, char *lines)
 {
        struct context *inc;
-       struct vcd_channel *vcd_ch;
-       GSList *l;
-       uint64_t timestamp, prev_timestamp;
-       unsigned int bit, i, j;
-       char **tokens;
+       int ret;
+       char **words;
+       size_t word_count, word_idx;
+       char *curr_word, *next_word, curr_first;
+       gboolean is_timestamp, is_section, is_real, is_multibit, is_singlebit;
+       uint64_t timestamp;
+       char *identifier, *endptr;
+       size_t count;
 
        inc = in->priv;
-       prev_timestamp = 0;
 
-       /* Read one space-delimited token at a time. */
-       tokens = g_strsplit_set(data, " \t\r\n", 0);
-       remove_empty_parts(tokens);
-       for (i = 0; tokens[i]; i++) {
+       /*
+        * Split the caller's text lines into a list of space separated
+        * words. Note that some of the branches consume the very next
+        * words as well, and assume that both adjacent words will be
+        * available when the first word is seen. This constraint applies
+        * to bit vector data, multi-bit integers and real (float) data,
+        * as well as single-bit data with whitespace before its
+        * identifier (if that's valid in VCD, we'd accept it here).
+        * The fact that callers always pass complete text lines should
+        * make this assumption acceptable.
+        */
+       ret = SR_OK;
+       words = split_text_line(inc, lines, &word_count);
+       for (word_idx = 0; word_idx < word_count; word_idx++) {
+               /*
+                * Make the next two words available, to simpilify code
+                * paths below. The second word is optional here.
+                */
+               curr_word = words[word_idx];
+               if (!curr_word && !curr_word[0])
+                       continue;
+               curr_first = g_ascii_tolower(curr_word[0]);
+               next_word = words[word_idx + 1];
+               if (next_word && !next_word[0])
+                       next_word = NULL;
+
+               /*
+                * Optionally skip some sections that can be interleaved
+                * with data (and may or may not be supported by this
+                * input module). If the section is not skipped but the
+                * $end keyword needs to get tracked, specifically handle
+                * this case, for improved robustness (still reject files
+                * which happen to use invalid syntax).
+                */
                if (inc->skip_until_end) {
-                       if (!strcmp(tokens[i], "$end")) {
+                       if (strcmp(curr_word, "$end") == 0) {
                                /* Done with unhandled/unknown section. */
+                               sr_dbg("done skipping until $end");
                                inc->skip_until_end = FALSE;
-                               break;
+                       } else {
+                               sr_spew("skipping word: %s", curr_word);
+                       }
+                       continue;
+               }
+               if (inc->ignore_end_keyword) {
+                       if (strcmp(curr_word, "$end") == 0) {
+                               sr_dbg("done ignoring $end keyword");
+                               inc->ignore_end_keyword = FALSE;
+                               continue;
+                       }
+               }
+
+               /*
+                * There may be $keyword sections inside the data part of
+                * the input file. Do inspect some of the sections' content
+                * but ignore their surrounding keywords. Silently skip
+                * unsupported section types (which transparently covers
+                * $comment sections).
+                */
+               is_section = curr_first == '$' && curr_word[1];
+               if (is_section) {
+                       gboolean inspect_data;
+
+                       inspect_data = FALSE;
+                       inspect_data |= g_strcmp0(curr_word, "$dumpvars") == 0;
+                       inspect_data |= g_strcmp0(curr_word, "$dumpon") == 0;
+                       inspect_data |= g_strcmp0(curr_word, "$dumpoff") == 0;
+                       if (inspect_data) {
+                               /* Ignore keywords, yet parse contents. */
+                               sr_dbg("%s section, will parse content", curr_word);
+                               inc->ignore_end_keyword = TRUE;
+                       } else {
+                               /* Ignore section from here up to $end. */
+                               sr_dbg("%s section, will skip until $end", curr_word);
+                               inc->skip_until_end = TRUE;
                        }
+                       continue;
                }
-               if (tokens[i][0] == '#' && g_ascii_isdigit(tokens[i][1])) {
-                       /* Numeric value beginning with # is a new timestamp value */
-                       timestamp = strtoull(tokens[i] + 1, NULL, 10);
 
-                       if (inc->downsample > 1)
-                               timestamp /= inc->downsample;
+               /*
+                * Numbers prefixed by '#' are timestamps, which translate
+                * to sigrok sample numbers. Apply optional downsampling,
+                * and apply the 'skip' logic. Check the recent timestamp
+                * for plausibility. Submit the corresponding number of
+                * samples of previously accumulated data values to the
+                * session feed.
+                */
+               is_timestamp = curr_first == '#' && g_ascii_isdigit(curr_word[1]);
+               if (is_timestamp) {
+                       endptr = NULL;
+                       timestamp = strtoull(&curr_word[1], &endptr, 10);
+                       if (!endptr || *endptr) {
+                               sr_err("Invalid timestamp: %s.", curr_word);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       sr_spew("Got timestamp: %" PRIu64, timestamp);
+                       ret = ts_stats_check(&inc->ts_stats, timestamp);
+                       if (ret != SR_OK)
+                               break;
+                       if (inc->options.downsample > 1) {
+                               timestamp /= inc->options.downsample;
+                               sr_spew("Downsampled timestamp: %" PRIu64, timestamp);
+                       }
 
                        /*
                         * Skip < 0 => skip until first timestamp.
                         * Skip = 0 => don't skip
                         * Skip > 0 => skip until timestamp >= skip.
                         */
-                       if (inc->skip < 0) {
-                               inc->skip = timestamp;
-                               prev_timestamp = timestamp;
-                       } else if (inc->skip > 0 && timestamp < (uint64_t)inc->skip) {
-                               prev_timestamp = inc->skip;
-                       } else if (timestamp == prev_timestamp) {
-                               /* Ignore repeated timestamps (e.g. sigrok outputs these) */
-                       } else {
-                               if (inc->compress != 0 && timestamp - prev_timestamp > inc->compress) {
-                                       /* Compress long idle periods */
-                                       prev_timestamp = timestamp - inc->compress;
+                       if (inc->options.skip_specified && !inc->use_skip) {
+                               sr_dbg("Seeding skip from user spec %" PRIu64,
+                                       inc->options.skip_starttime);
+                               inc->prev_timestamp = inc->options.skip_starttime;
+                               inc->use_skip = TRUE;
+                       }
+                       if (!inc->use_skip) {
+                               sr_dbg("Seeding skip from first timestamp");
+                               inc->options.skip_starttime = timestamp;
+                               inc->prev_timestamp = timestamp;
+                               inc->use_skip = TRUE;
+                               continue;
+                       }
+                       if (inc->options.skip_starttime && timestamp < inc->options.skip_starttime) {
+                               sr_spew("Timestamp skipped, before user spec");
+                               inc->prev_timestamp = inc->options.skip_starttime;
+                               continue;
+                       }
+                       if (timestamp == inc->prev_timestamp) {
+                               /*
+                                * Ignore repeated timestamps (e.g. sigrok
+                                * outputs these). Can also happen when
+                                * downsampling makes distinct input values
+                                * end up at the same scaled down value.
+                                * Also transparently covers the initial
+                                * timestamp.
+                                */
+                               sr_spew("Timestamp is identical to previous timestamp");
+                               continue;
+                       }
+                       if (timestamp < inc->prev_timestamp) {
+                               sr_err("Invalid timestamp: %" PRIu64 " (leap backwards).", timestamp);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       if (inc->options.compress) {
+                               /* Compress long idle periods */
+                               count = timestamp - inc->prev_timestamp;
+                               if (count > inc->options.compress) {
+                                       sr_dbg("Long idle period, compressing");
+                                       count = timestamp - inc->options.compress;
+                                       inc->prev_timestamp = count;
                                }
-
-                               sr_dbg("New timestamp: %" PRIu64, timestamp);
-
-                               /* Generate samples from prev_timestamp up to timestamp - 1. */
-                               add_samples(in, timestamp - prev_timestamp);
-                               prev_timestamp = timestamp;
                        }
-               } else if (tokens[i][0] == '$' && tokens[i][1] != '\0') {
-                       /*
-                        * This is probably a $dumpvars, $comment or similar.
-                        * $dump* contain useful data.
-                        */
-                       if (g_strcmp0(tokens[i], "$dumpvars") == 0
-                                       || g_strcmp0(tokens[i], "$dumpon") == 0
-                                       || g_strcmp0(tokens[i], "$dumpoff") == 0
-                                       || g_strcmp0(tokens[i], "$end") == 0) {
-                               /* Ignore, parse contents as normally. */
-                       } else {
-                               /* Ignore this and future lines until $end. */
-                               inc->skip_until_end = TRUE;
+
+                       /* Generate samples from prev_timestamp up to timestamp - 1. */
+                       count = timestamp - inc->prev_timestamp;
+                       sr_spew("Got a new timestamp, feeding %zu samples", count);
+                       add_samples(in, count, FALSE);
+                       inc->prev_timestamp = timestamp;
+                       inc->data_after_timestamp = FALSE;
+                       continue;
+               }
+               inc->data_after_timestamp = TRUE;
+
+               /*
+                * Data values come in different formats, are associated
+                * with channel identifiers, and correspond to the period
+                * of time from the most recent timestamp to the next
+                * timestamp.
+                *
+                * Supported input data formats are:
+                * - R<value> <sep> <id> (analog channel, VCD type 'real').
+                * - B<value> <sep> <id> (analog channel, VCD type 'integer').
+                * - B<value> <sep> <id> (logic channels, VCD bit vectors).
+                * - <value> <id> (logic channel, VCD single-bit values).
+                *
+                * Input values can be:
+                * - Floating point numbers.
+                * - Bit strings (which covers multi-bit aka integers
+                *   as well as vectors).
+                * - Single bits.
+                *
+                * Things to note:
+                * - Individual bits can be 0/1 which is supported by
+                *   libsigrok, or x or z which is treated like 0 here
+                *   (sigrok lacks support for ternary logic, neither is
+                *   there support for the full IEEE set of values).
+                * - Single-bit values typically won't be separated from
+                *   the signal identifer, multi-bit values and floats
+                *   are separated (will reference the next word). This
+                *   implementation silently accepts separators for
+                *   single-bit values, too.
+                */
+               is_real = curr_first == 'r' && curr_word[1];
+               is_multibit = curr_first == 'b' && curr_word[1];
+               is_singlebit = curr_first == '0' || curr_first == '1';
+               is_singlebit |= curr_first == 'l' || curr_first == 'h';
+               is_singlebit |= curr_first == 'x' || curr_first == 'z';
+               is_singlebit |= curr_first == 'u' || curr_first == '-';
+               if (is_real) {
+                       char *real_text;
+                       float real_val;
+
+                       real_text = &curr_word[1];
+                       identifier = next_word;
+                       word_idx++;
+                       if (!*real_text || !identifier || !*identifier) {
+                               sr_err("Unexpected real format.");
+                               ret = SR_ERR_DATA;
                                break;
                        }
-               } else if (strchr("bBrR", tokens[i][0]) != NULL) {
-                       sr_dbg("Vector values not supported yet");
-                       if (!tokens[++i])
-                               /* No tokens left, bail out */
+                       sr_spew("Got real data %s for id '%s'.",
+                               real_text, identifier);
+                       if (sr_atof_ascii(real_text, &real_val) != SR_OK) {
+                               sr_err("Cannot convert value: %s.", real_text);
+                               ret = SR_ERR_DATA;
                                break;
-                       else
-                               /* Process next token */
-                               continue;
-               } else if (strchr("01xXzZ", tokens[i][0]) != NULL) {
-                       char *identifier;
+                       }
+                       process_real(inc, identifier, real_val);
+                       continue;
+               }
+               if (is_multibit) {
+                       char *bits_text_start;
+                       size_t bit_count;
+                       char *bits_text, bit_char;
+                       uint8_t bit_value;
+                       uint8_t *value_ptr, value_mask;
+                       GString *bits_val_text;
+
+                       /* TODO
+                        * Fold in single-bit code path here? To re-use
+                        * the X/Z support. Current redundancy is few so
+                        * there is little pressure to unify code paths.
+                        * Also multi-bit handling is often different
+                        * from single-bit handling, so the "unified"
+                        * path would often check for special cases. So
+                        * we may never unify code paths at all here.
+                        */
+                       bits_text = &curr_word[1];
+                       identifier = next_word;
+                       word_idx++;
 
-                       /* A new 1-bit sample value */
-                       bit = (tokens[i][0] == '1');
+                       if (!*bits_text || !identifier || !*identifier) {
+                               sr_err("Unexpected integer/vector format.");
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       sr_spew("Got integer/vector data %s for id '%s'.",
+                               bits_text, identifier);
 
                        /*
-                        * The identifier is either the next character, or, if
-                        * there was whitespace after the bit, the next token.
+                        * Accept a bit string of arbitrary length (sort
+                        * of, within the limits of the previously setup
+                        * conversion buffer). The input text omits the
+                        * leading zeroes, hence we convert from end to
+                        * the start, to get the significant bits. There
+                        * should only be errors for invalid input, or
+                        * for input that is rather strange (data holds
+                        * more bits than the signal's declaration in
+                        * the header suggested). Silently accept data
+                        * that fits in the conversion buffer, and has
+                        * more significant bits than the signal's type
+                        * (that'd be non-sence yet acceptable input).
                         */
-                       if (tokens[i][1] == '\0') {
-                               if (!tokens[++i]) {
-                                       sr_dbg("Identifier missing!");
+                       bits_text_start = bits_text;
+                       bits_text += strlen(bits_text);
+                       bit_count = bits_text - bits_text_start;
+                       if (bit_count > inc->conv_bits.max_bits) {
+                               sr_err("Value exceeds conversion buffer: %s",
+                                       bits_text_start);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       memset(inc->conv_bits.value, 0, inc->conv_bits.unit_size);
+                       value_ptr = &inc->conv_bits.value[0];
+                       value_mask = 1 << 0;
+                       inc->conv_bits.sig_count = 0;
+                       while (bits_text > bits_text_start) {
+                               inc->conv_bits.sig_count++;
+                               bit_char = *(--bits_text);
+                               bit_value = vcd_char_to_value(bit_char, NULL);
+                               if (bit_value == 0) {
+                                       /* EMPTY */
+                               } else if (bit_value == 1) {
+                                       *value_ptr |= value_mask;
+                               } else {
+                                       inc->conv_bits.sig_count = 0;
                                        break;
                                }
-                               identifier = tokens[i];
-                       } else {
-                               identifier = tokens[i] + 1;
+                               value_mask <<= 1;
+                               if (!value_mask) {
+                                       value_ptr++;
+                                       value_mask = 1 << 0;
+                               }
+                       }
+                       if (!inc->conv_bits.sig_count) {
+                               sr_err("Unexpected vector format: %s",
+                                       bits_text_start);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
+                               bits_val_text = sr_hexdump_new(inc->conv_bits.value,
+                                       value_ptr - inc->conv_bits.value + 1);
+                               sr_spew("Vector value: %s.", bits_val_text->str);
+                               sr_hexdump_free(bits_val_text);
                        }
 
-                       for (j = 0, l = inc->channels; j < inc->channelcount && l; j++, l = l->next) {
-                               vcd_ch = l->data;
-                               if (g_strcmp0(identifier, vcd_ch->identifier) == 0) {
-                                       /* Found our channel */
-                                       size_t byte_idx = (j / 8);
-                                       size_t bit_idx = j - 8 * byte_idx;
-                                       if (bit)
-                                               inc->current_levels[byte_idx] |= (uint8_t)1 << bit_idx;
-                                       else
-                                               inc->current_levels[byte_idx] &= ~((uint8_t)1 << bit_idx);
-                                       break;
-                               }
+                       process_bits(inc, identifier,
+                               inc->conv_bits.value, inc->conv_bits.sig_count);
+                       continue;
+               }
+               if (is_singlebit) {
+                       char *bits_text, bit_char;
+                       uint8_t bit_value;
+
+                       /* Get the value text, and signal identifier. */
+                       bits_text = &curr_word[0];
+                       bit_char = *bits_text;
+                       if (!bit_char) {
+                               sr_err("Bit value missing.");
+                               ret = SR_ERR_DATA;
+                               break;
                        }
-                       if (j == inc->channelcount)
-                               sr_dbg("Did not find channel for identifier '%s'.", identifier);
-               } else {
-                       sr_warn("Skipping unknown token '%s'.", tokens[i]);
+                       identifier = ++bits_text;
+                       if (!*identifier) {
+                               identifier = next_word;
+                               word_idx++;
+                       }
+                       if (!identifier || !*identifier) {
+                               sr_err("Identifier missing.");
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+
+                       /* Convert value text to single-bit number. */
+                       bit_value = vcd_char_to_value(bit_char, NULL);
+                       if (bit_value != 0 && bit_value != 1) {
+                               sr_err("Unsupported bit value '%c'.", bit_char);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       inc->conv_bits.value[0] = bit_value;
+                       process_bits(inc, identifier, inc->conv_bits.value, 1);
+                       continue;
                }
+
+               /* Design choice: Consider unsupported input fatal. */
+               sr_err("Unknown token '%s'.", curr_word);
+               ret = SR_ERR_DATA;
+               break;
        }
-       g_strfreev(tokens);
+       free_text_split(inc, words);
+
+       return ret;
 }
 
-static int init(struct sr_input *in, GHashTable *options)
+static int process_buffer(struct sr_input *in, gboolean is_eof)
 {
-       int num_channels, i;
-       char name[16];
        struct context *inc;
+       uint64_t samplerate;
+       GVariant *gvar;
+       int ret;
+       char *rdptr, *endptr, *trimptr;
+       size_t rdlen;
 
-       num_channels = g_variant_get_int32(g_hash_table_lookup(options, "numchannels"));
-       if (num_channels < 1) {
-               sr_err("Invalid value for numchannels: must be at least 1.");
-               return SR_ERR_ARG;
-       }
-       inc = in->priv = g_malloc0(sizeof(struct context));
-       inc->maxchannels = num_channels;
-
-       inc->downsample = g_variant_get_int32(g_hash_table_lookup(options, "downsample"));
-       if (inc->downsample < 1)
-               inc->downsample = 1;
+       inc = in->priv;
 
-       inc->compress = g_variant_get_int32(g_hash_table_lookup(options, "compress"));
-       inc->skip = g_variant_get_int32(g_hash_table_lookup(options, "skip"));
-       inc->skip /= inc->downsample;
+       /* Send feed header and samplerate (once) before sample data. */
+       if (!inc->started) {
+               std_session_send_df_header(in->sdi);
 
-       in->sdi = g_malloc0(sizeof(struct sr_dev_inst));
-       in->priv = inc;
+               samplerate = inc->samplerate / inc->options.downsample;
+               if (samplerate) {
+                       gvar = g_variant_new_uint64(samplerate);
+                       sr_session_send_meta(in->sdi, SR_CONF_SAMPLERATE, gvar);
+               }
 
-       inc->buffer = g_malloc(CHUNKSIZE);
+               inc->started = TRUE;
+       }
 
-       for (i = 0; i < num_channels; i++) {
-               snprintf(name, 16, "%d", i);
-               sr_channel_new(in->sdi, i, SR_CHANNEL_LOGIC, TRUE, name);
+       /*
+        * Workaround broken generators which output incomplete text
+        * lines. Enforce the trailing line feed. Proper input is not
+        * harmed by another empty line of input data.
+        */
+       if (is_eof)
+               g_string_append_c(in->buf, '\n');
+
+       /* Find and process complete text lines in the input data. */
+       ret = SR_OK;
+       rdptr = in->buf->str;
+       while (TRUE) {
+               rdlen = &in->buf->str[in->buf->len] - rdptr;
+               endptr = g_strstr_len(rdptr, rdlen, "\n");
+               if (!endptr)
+                       break;
+               trimptr = endptr;
+               *endptr++ = '\0';
+               while (g_ascii_isspace(*rdptr))
+                       rdptr++;
+               while (trimptr > rdptr && g_ascii_isspace(trimptr[-1]))
+                       *(--trimptr) = '\0';
+               if (!*rdptr) {
+                       rdptr = endptr;
+                       continue;
+               }
+               ret = parse_textline(in, rdptr);
+               rdptr = endptr;
+               if (ret != SR_OK)
+                       break;
        }
+       rdlen = rdptr - in->buf->str;
+       g_string_erase(in->buf, 0, rdlen);
 
-       return SR_OK;
+       return ret;
 }
 
-static gboolean have_header(GString *buf)
+static int format_match(GHashTable *metadata, unsigned int *confidence)
 {
-       unsigned int pos;
-       char *p;
+       GString *buf, *tmpbuf;
+       gboolean status;
+       char *name, *contents;
 
-       if (!(p = g_strstr_len(buf->str, buf->len, "$enddefinitions")))
-               return FALSE;
-       pos = p - buf->str + 15;
-       while (pos < buf->len - 4 && g_ascii_isspace(buf->str[pos]))
-               pos++;
-       if (!strncmp(buf->str + pos, "$end", 4))
-               return TRUE;
+       buf = g_hash_table_lookup(metadata,
+               GINT_TO_POINTER(SR_INPUT_META_HEADER));
+       tmpbuf = g_string_new_len(buf->str, buf->len);
+
+       /*
+        * If we can parse the first section correctly, then it is
+        * assumed that the input is in VCD format.
+        */
+       check_remove_bom(tmpbuf);
+       status = parse_section(tmpbuf, &name, &contents);
+       g_string_free(tmpbuf, TRUE);
+       g_free(name);
+       g_free(contents);
 
-       return FALSE;
+       if (!status)
+               return SR_ERR;
+
+       *confidence = 1;
+       return SR_OK;
 }
 
-static int process_buffer(struct sr_input *in)
+static int init(struct sr_input *in, GHashTable *options)
 {
-       struct sr_datafeed_packet packet;
-       struct sr_datafeed_meta meta;
-       struct sr_config *src;
        struct context *inc;
-       uint64_t samplerate;
-       char *p;
+       GVariant *data;
 
-       inc = in->priv;
-       if (!inc->started) {
-               std_session_send_df_header(in->sdi, LOG_PREFIX);
+       inc = g_malloc0(sizeof(*inc));
 
-               packet.type = SR_DF_META;
-               packet.payload = &meta;
-               samplerate = inc->samplerate / inc->downsample;
-               src = sr_config_new(SR_CONF_SAMPLERATE, g_variant_new_uint64(samplerate));
-               meta.config = g_slist_append(NULL, src);
-               sr_session_send(in->sdi, &packet);
-               g_slist_free(meta.config);
-               sr_config_free(src);
+       data = g_hash_table_lookup(options, "numchannels");
+       inc->options.maxchannels = g_variant_get_uint32(data);
 
-               inc->started = TRUE;
-       }
+       data = g_hash_table_lookup(options, "downsample");
+       inc->options.downsample = g_variant_get_uint64(data);
+       if (inc->options.downsample < 1)
+               inc->options.downsample = 1;
 
-       while ((p = g_strrstr_len(in->buf->str, in->buf->len, "\n"))) {
-               *p = '\0';
-               g_strstrip(in->buf->str);
-               if (in->buf->str[0] != '\0')
-                       parse_contents(in, in->buf->str);
-               g_string_erase(in->buf, 0, p - in->buf->str + 1);
+       data = g_hash_table_lookup(options, "compress");
+       inc->options.compress = g_variant_get_uint64(data);
+       inc->options.compress /= inc->options.downsample;
+
+       data = g_hash_table_lookup(options, "skip");
+       if (data) {
+               inc->options.skip_specified = TRUE;
+               inc->options.skip_starttime = g_variant_get_uint64(data);
+               if (inc->options.skip_starttime == ~UINT64_C(0)) {
+                       inc->options.skip_specified = FALSE;
+                       inc->options.skip_starttime = 0;
+               }
+               inc->options.skip_starttime /= inc->options.downsample;
        }
 
+       in->sdi = g_malloc0(sizeof(*in->sdi));
+       in->priv = inc;
+
+       inc->scope_prefix = g_string_new("\0");
+
        return SR_OK;
 }
 
@@ -548,46 +2014,55 @@ static int receive(struct sr_input *in, GString *buf)
        struct context *inc;
        int ret;
 
+       inc = in->priv;
+
+       /* Collect all input chunks, potential deferred processing. */
        g_string_append_len(in->buf, buf->str, buf->len);
+       if (!inc->got_header && in->buf->len == buf->len)
+               check_remove_bom(in->buf);
 
-       inc = in->priv;
+       /* Must complete reception of the VCD header first. */
        if (!inc->got_header) {
                if (!have_header(in->buf))
                        return SR_OK;
-               if (!parse_header(in, in->buf))
-                       /* There was a header in there, but it was malformed. */
-                       return SR_ERR;
-
-               in->sdi_ready = TRUE;
+               ret = parse_header(in, in->buf);
+               if (ret != SR_OK)
+                       return ret;
                /* sdi is ready, notify frontend. */
+               in->sdi_ready = TRUE;
                return SR_OK;
        }
 
-       ret = process_buffer(in);
+       /* Process sample data. */
+       ret = process_buffer(in, FALSE);
 
        return ret;
 }
 
 static int end(struct sr_input *in)
 {
-       struct sr_datafeed_packet packet;
        struct context *inc;
        int ret;
+       size_t count;
 
        inc = in->priv;
 
+       /* Must complete processing of previously received chunks. */
        if (in->sdi_ready)
-               ret = process_buffer(in);
+               ret = process_buffer(in, TRUE);
        else
                ret = SR_OK;
 
-       /* Send any samples that haven't been sent yet. */
-       send_buffer(in);
+       /* Flush most recently queued sample data when EOF is seen. */
+       count = inc->data_after_timestamp ? 1 : 0;
+       add_samples(in, count, TRUE);
 
-       if (inc->started) {
-               packet.type = SR_DF_END;
-               sr_session_send(in->sdi, &packet);
-       }
+       /* Optionally suggest downsampling after all input data was seen. */
+       (void)ts_stats_post(inc, !inc->data_after_timestamp);
+
+       /* Must send DF_END when DF_HEADER was sent before. */
+       if (inc->started)
+               std_session_send_df_end(in->sdi);
 
        return ret;
 }
@@ -597,28 +2072,91 @@ static void cleanup(struct sr_input *in)
        struct context *inc;
 
        inc = in->priv;
+
+       keep_header_for_reread(in);
+
        g_slist_free_full(inc->channels, free_channel);
-       g_free(inc->buffer);
-       inc->buffer = NULL;
-       g_free(inc->current_levels);
-       inc->current_levels = NULL;
+       inc->channels = NULL;
+       feed_queue_logic_free(inc->feed_logic);
+       inc->feed_logic = NULL;
+       g_free(inc->conv_bits.value);
+       inc->conv_bits.value = NULL;
+       g_free(inc->current_logic);
+       inc->current_logic = NULL;
+       g_free(inc->current_floats);
+       inc->current_floats = NULL;
+       g_string_free(inc->scope_prefix, TRUE);
+       inc->scope_prefix = NULL;
+       g_slist_free_full(inc->ignored_signals, g_free);
+       inc->ignored_signals = NULL;
+       free_text_split(inc, NULL);
 }
 
+static int reset(struct sr_input *in)
+{
+       struct context *inc;
+       struct vcd_user_opt save;
+       struct vcd_prev prev;
+
+       inc = in->priv;
+
+       /* Relase previously allocated resources. */
+       cleanup(in);
+       g_string_truncate(in->buf, 0);
+
+       /* Restore part of the context, init() won't run again. */
+       save = inc->options;
+       prev = inc->prev;
+       memset(inc, 0, sizeof(*inc));
+       inc->options = save;
+       inc->prev = prev;
+       inc->scope_prefix = g_string_new("\0");
+
+       return SR_OK;
+}
+
+enum vcd_option_t {
+       OPT_NUM_CHANS,
+       OPT_DOWN_SAMPLE,
+       OPT_SKIP_COUNT,
+       OPT_COMPRESS,
+       OPT_MAX,
+};
+
 static struct sr_option options[] = {
-       { "numchannels", "Number of channels", "Number of channels", NULL, NULL },
-       { "skip", "Skip", "Skip until timestamp", NULL, NULL },
-       { "downsample", "Downsample", "Divide samplerate by factor", NULL, NULL },
-       { "compress", "Compress", "Compress idle periods longer than this value", NULL, NULL },
-       ALL_ZERO
+       [OPT_NUM_CHANS] = {
+               "numchannels", "Max number of sigrok channels",
+               "The maximum number of sigrok channels to create for VCD input signals.",
+               NULL, NULL,
+       },
+       [OPT_DOWN_SAMPLE] = {
+               "downsample", "Downsampling factor",
+               "Downsample the input file's samplerate, i.e. divide by the specified factor.",
+               NULL, NULL,
+       },
+       [OPT_SKIP_COUNT] = {
+               "skip", "Skip this many initial samples",
+               "Skip samples until the specified timestamp. "
+               "By default samples start at the first timestamp in the file. "
+               "Value 0 creates samples starting at timestamp 0. "
+               "Values above 0 only start processing at the given timestamp.",
+               NULL, NULL,
+       },
+       [OPT_COMPRESS] = {
+               "compress", "Compress idle periods",
+               "Compress idle periods which are longer than the specified number of timescale ticks.",
+               NULL, NULL,
+       },
+       [OPT_MAX] = ALL_ZERO,
 };
 
 static const struct sr_option *get_options(void)
 {
        if (!options[0].def) {
-               options[0].def = g_variant_ref_sink(g_variant_new_int32(DEFAULT_NUM_CHANNELS));
-               options[1].def = g_variant_ref_sink(g_variant_new_int32(-1));
-               options[2].def = g_variant_ref_sink(g_variant_new_int32(1));
-               options[3].def = g_variant_ref_sink(g_variant_new_int32(0));
+               options[OPT_NUM_CHANS].def = g_variant_ref_sink(g_variant_new_uint32(0));
+               options[OPT_DOWN_SAMPLE].def = g_variant_ref_sink(g_variant_new_uint64(1));
+               options[OPT_SKIP_COUNT].def = g_variant_ref_sink(g_variant_new_uint64(~UINT64_C(0)));
+               options[OPT_COMPRESS].def = g_variant_ref_sink(g_variant_new_uint64(0));
        }
 
        return options;
@@ -627,7 +2165,7 @@ static const struct sr_option *get_options(void)
 SR_PRIV struct sr_input_module input_vcd = {
        .id = "vcd",
        .name = "VCD",
-       .desc = "Value Change Dump",
+       .desc = "Value Change Dump data",
        .exts = (const char*[]){"vcd", NULL},
        .metadata = { SR_INPUT_META_HEADER | SR_INPUT_META_REQUIRED },
        .options = get_options,
@@ -636,4 +2174,5 @@ SR_PRIV struct sr_input_module input_vcd = {
        .receive = receive,
        .end = end,
        .cleanup = cleanup,
+       .reset = reset,
 };