dvd-sector-copy.c: Add substantial commentary.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 9 Apr 2022 17:15:10 +0000 (18:15 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 9 Apr 2022 17:15:10 +0000 (18:15 +0100)
And light reformatting.

dvd-sector-copy.c

index d60e1c1..3583770 100644 (file)
@@ -1,5 +1,34 @@
+/* -*-c-*-
+ *
+ * Make an unscrambled copy of a DVD.
+ *
+ * (c) 2022 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the DVD ripping toolset.
+ *
+ * DVDrip is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * DVDrip is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DVDrip.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
 #include "lib.h"
 
+/*----- Program usage summary ---------------------------------------------*/
+
 static void usage(FILE *fp)
 {
   fprintf(fp,
@@ -8,25 +37,57 @@ static void usage(FILE *fp)
          prog);
 }
 
+/*----- Random utilities --------------------------------------------------*/
+
 #define PRF_HYPHEN 1u
 static int parse_range(const char *p, unsigned f,
                       secaddr *start_out, secaddr *end_out)
+       /* Parse a range of sectors from the string P.  If successful, store
+        * the specified start sector address in *START_OUT and the end
+        * address in *END_OUT, and return zero.  On failure, return -1;
+        * *START_OUT and/or *END_OUT are clobbered.
+        *
+        * The acceptable syntax depends on the flags.
+        *
+        *   * The `PRF_HYPHEN' syntax is intended for use on the
+        *     command-line.  It accepts `[START]-[END]'; if the start and/or
+        *     end addresses are omitted then *START_OUT and/or *END_OUT are
+        *     left unchanged.
+        *
+        *   * The default syntax matches what's written to the bad-sector
+        *     output files.  It accepts `START END [# COMMENT]'.
+        */
 {
   char *q;
   int err, rc;
   unsigned long start, end;
 
+  /* Save any existing error code. */
   err = errno;
 
+  /* Parse the start address. */
   if (ISDIGIT(*p)) {
+    /* We found a digit: this is a good start.  Convert the integer, check
+     * that it's in range, save it.
+     */
+
     start = strtoul(p, &q, 0);
     if (errno || start >= SECLIMIT) { rc = -1; goto end; }
     *start_out = start; p = q;
-  } else if (!(f&PRF_HYPHEN))
-    { rc = -1; goto end; }
-  else
+  } else if (!(f&PRF_HYPHEN)) {
+    /* No digit.  We're parsing the map-file syntax, so this is an error. */
+
+    rc = -1; goto end;
+  } else {
+    /* We're parsing the command-line syntax, so this is OK.  Set our
+     * internal idea of the position for the range check later, but don't
+     * alter the caller's variables.
+     */
+
     start = 0;
+  }
 
+  /* Parse the delimiter. */
   if (f&PRF_HYPHEN) {
     if (*p != '-')  { rc = -1; goto end; }
     p++;
@@ -35,56 +96,177 @@ static int parse_range(const char *p, unsigned f,
     do p++; while (ISSPACE(*p));
   }
 
+  /* Parse the end address. */
   if (ISDIGIT(*p)) {
+    /* We found a digit.  Parse the integer and check that it's strictly
+     * larger than the start address.
+     */
+
     end = strtoul(p, &q, 0);
     if (errno || end > SECLIMIT || end < start) { rc = -1; goto end; }
     *end_out = end; p = q;
-  } else if (!(f&PRF_HYPHEN))
-    { rc = -1; goto end; }
+  } else if (!(f&PRF_HYPHEN)) {
+    /* No digit.  We're parsing the file syntax, so this is an error. */
+
+    rc = -1; goto end;
+  }
 
+  /* In the file syntax, we're now allowed whitespace, so skip past that. */
   if (!(f&PRF_HYPHEN)) while (ISSPACE(*p)) p++;
+
+  /* Check that there's nothing else.  The file syntax allows a trailing
+   * comment here, but the command-line syntax doesn't.
+   */
   if (*p && ((f&PRF_HYPHEN) || *p != '#')) { rc = -1; goto end; }
 
+  /* All done! */
   rc = 0;
 end:
   errno = err;
   return (rc);
 }
 
+/*----- A few words about the overall approach ----------------------------*
+ *
+ * The objective is to produce a working copy of the input (commercial,
+ * pressed) DVD disc, only with all of the scrambled video data unscrambled
+ * so that it can be read without the need for cracking CSS keys, which, in
+ * the absence of a cooperative drive with access to the key tables in the
+ * disc lead-in data -- which we /don't/ copy -- is often slow and prone to
+ * failure.  Producing a sector-by-sector image preserves all of the menus
+ * and special features, and also any other bonus data stored in the
+ * filesystem for use by computers, such as PDF scripts.  DVD images are
+ * large because DVD video is inefficiently compressed by modern standards,
+ * but disk space is cheap and the tradeoff seems worthwhile to me.
+ *
+ * The approach is, in essence, simple: start at the beginning of the disc,
+ * reading sectors into a buffer and writing them to the output file, and
+ * continue until we reach the end.  But we must cope with scrambled video
+ * files.  Fortunately, `libdvdread' knows how to deal with these, and will
+ * tell us where they are on the disc.
+ *
+ * Given this information, we build a table of `events', with the sector
+ * numbers at which they occur.  An `event' might be something like `such-
+ * and-such a video file began' or `such-and-such a file ended'.  Chunks of
+ * disc between events can be read using the same strategy -- either reading
+ * unscrambled sectors directly from the block device, or decrypting
+ * scrambled sectors through `libdvdread' -- while at sector boundaries we
+ * might need to change strategy.
+ *
+ * Note that files can /overlap/.  The DVD spec says that this can't happen,
+ * and that the data for video titles is laid out with higher-numbered
+ * titlesets occupying higher-numbered sectors, but it does anyway.  I think
+ * this is intended to frustrate copiers like `dvdbackup' which try to copy
+ * the DVD files into a directory on the filesystem.  The result is that they
+ * copy the same sectors into multiple, very large files, and turn an 8 GB
+ * DVD image into a 60 GB directory.  (The reused regions often also contain
+ * intentionally bad sectors, so you have to wait for the drive to fail the
+ * same sectors over and over again.  This is no fun.)  As far as I know,
+ * files are either disjoint or coincident, but more complex arrangements are
+ * possible in principle.  Also, I guess it's possible that the same sector
+ * should be decrypted with different keys depending on which titleset we're
+ * considering it being part of, but (a) DVD CSS keys aren't long enough to
+ * do this very well, and (b) I'm not aware of this actually being a thing.
+ * (Indeed, `libdvdcss' indexes keys by start sector, so such a disc probably
+ * wouldn't play back properly through VLC or `mpv'.)
+ *
+ * There's an additional consideration.  We want to be able to fill in an
+ * ouptut image file incrementally, in several runs.  A run can be
+ * interrupted for lots of reasons (e.g., a faster drive might have become
+ * available; it might be beneficial to switch to a more forgiving drive; it
+ * might be necessary to stop and clean the disc; the output filesystem might
+ * have become full; ...).  And discs don't always read perfectly: some discs
+ * are damaged and have areas which can't be read; some discs (I'm looking at
+ * you, Sony, Disney, Lionsgate, and E-One) have intentional bad sectors,
+ * presumably specifically to make my life annoying.  So we have other events
+ * which say things like `start writing stuff to the output' or `stop writing
+ * things to the output'.  And we have a rather elaborate algorithm for
+ * trying to skip past a region of bad blocks, because drives get /really/
+ * slow when reading bad sectors.
+ */
+
+/*----- The file and event tables -----------------------------------------*/
+
 #define MAXFILES (1 + 2*99 + 1)
+       /* How many (interesting) files there can be.  This counts the
+        * magical `raw' file which refers to direct disc access, the master
+        * menu file, and 99 possible menu and titleset pairs.  (A titleset
+        * can be split into 9 parts in order to keep each file below a
+        * gigabyte in size, but the rules require that the parts together
+        * form a single contiguous chunk on the disc, in the right order, so
+        * we treat them as a single file.  We check this in `put_title'
+        * below, just in case some disc somewhere tries to be awkward, but I
+        * don't have a disc like that in my collection, and I doubt it would
+        * work very well.)
+        */
+
 struct file {
-  ident id;
-  secaddr start, end;
+       /* An interesting DVD file.  It has a name, encoded as an `ident'
+        * (see `lib.h'), and start and end sectors.  (The `end' here, as
+        * everywhere in this code, is /exclusive/, so that the file's length
+        * is simply end - start.)
+        */
+
+  ident id;                            /* file name */
+  secaddr start, end;                  /* start (inclusive) and end
+                                        * (exclusive) sector numbers */
+};
+DEFVEC(file_v, struct file);           /* a vector of files */
+static file_v filetab = VEC_INIT;      /* the file table */
+
+enum {
+       /* Event codes.  The ordering of these is important, because we use
+        * them to tie-break comparisons of events happening at the same
+        * sector when we sort the event queue.
+        */
+
+  EV_STOP,                             /* stop copying stuff to output */
+  EV_BEGIN,                            /* a (maybe scrambled) file begins */
+  EV_END,                              /* a file ends */
+  EV_WRITE                             /* start copying stuff to output */
 };
-DEFVEC(file_v, struct file);
-static file_v filetab = VEC_INIT;
 
-enum { EV_STOP, EV_BEGIN, EV_END, EV_WRITE };
 struct event {
-  unsigned char ev, file;
-  secaddr pos;
+       /* An event. */
+
+  unsigned char ev;                    /* event code (`EV_...') */
+  unsigned char file;                  /* the file (`EV_BEGIN', `EV_END');
+                                        *   index into `filetab' */
+  secaddr pos;                         /* the sector at which it happens */
 };
-DEFVEC(event_v, struct event);
-static event_v eventq = VEC_INIT;
+DEFVEC(event_v, struct event);         /* a vector of events */
+static event_v eventq = VEC_INIT;      /* the event queue */
 
 static int compare_event(const void *a, const void *b)
+       /* A `qsort' comparison function for events.  Event A sorts earlier
+        * than event B iff A's sector number is smaller than B's, or A's
+        * event code is less than B's.
+        */
 {
   const struct event *eva = a, *evb = b;
 
+  /* Primary ordering by position. */
   if (eva->pos < evb->pos) return (-1);
   else if (eva->pos > evb->pos) return (+1);
 
+  /* Secondary ordering by event code. */
   if (eva->ev < evb->ev) return (-1);
   else if (eva->ev > evb->ev) return (+1);
 
+  /* We currently have a final tie-break on file numbers so that the ordering
+   * is deterministic, but this is an arbitrary choice that shouldn't be
+   * relied upon.
+   */
   if (eva->file < evb->file) return (-1);
   else if (eva->file > evb->file) return (+1);
 
+  /* These events are equal. */
   return (0);
 }
 
 #ifdef DEBUG
 static void dump_eventq(const char *what)
+       /* Dump the event queue, labelling the output with WHAT. */
 {
   unsigned i;
   const struct event *ev;
@@ -118,21 +300,45 @@ static void dump_eventq(const char *what)
 
 typedef uint_least32_t bits;
 static bits live[(MAXFILES + 31)/32];
+       /* A bitmap which keeps track of which files are currently `active',
+        * i.e., that contain the sector we're currently thinking about.  We
+        * set and clear these bits as we encounter `EV_BEGIN' and `EV_END'
+        * events.
+        */
 
 static inline int livep(unsigned i)
+       /* Return whether file I is active. */
   { return (live[i/32]&((bits)1 << (i%32))); }
+
 static inline void set_live(unsigned i)
+       /* Note that we've seen the start of file I. */
   { live[i/32] |= (bits)1 << (i%32); }
+
 static inline void clear_live(unsigned i)
+       /* Note that we've seen the end of file I. */
   { live[i/32] &= ~((bits)1 << (i%32)); }
+
 static inline int least_live(void)
+       /* Return the smallest index for any active file.  This is going to
+        * be the file that we ask `libdvdread' to unscramble for us.  This
+        * is important: the imaginary `raw' file that represents the entire
+        * block device has the highest index, and we want any actual video
+        * file to be used in preference so that we unscramble the data.
+        */
 {
   unsigned i, n = (filetab.n + 32)/32;
   bits b;
 
+  /* First part: find the first nonzero word in the `live' table. */
   for (i = 0; i < n; i++) { b = live[i]; if (b) goto found; }
   return (-1);
+
 found:
+  /* Second part: identify which bit in this word is nonzero.  First, see if
+   * the bottom 16 bits are clear: if so, shift down and add 16 to the
+   * total.  Now we know that the first set bit is indeed in the low 16
+   * bits, so see whether the low 8 bits are clear, and so on.
+   */
   i *= 32;
   if (!(b&0x0000ffff)) { b >>= 16; i += 16; }
   if (!(b&0x000000ff)) { b >>=  8; i +=  8; }
@@ -140,10 +346,17 @@ found:
   if (!(b&0x00000003)) { b >>=  2; i +=  2; }
   if (!(b&0x00000001)) { b >>=  1; i +=  1; }
   assert(b&1);
+
+  /* Done. */
   return (i);
 }
 
 static void put_event(unsigned evtype, unsigned file, secaddr pos)
+       /* Add an event to the queue, with type EVTYPE, for the given FILE,
+        * and at sector POS.  You can add events in any order because we'll
+        * sort them later.  For `EV_WRITE' and `EV_STOP' events, the FILE
+        * doesn't matter: use zero for concreteness.
+        */
 {
   struct event *ev;
 
@@ -152,6 +365,9 @@ static void put_event(unsigned evtype, unsigned file, secaddr pos)
 }
 
 static void put_file(ident id, secaddr start, secaddr end)
+       /* Add a (VOB) file to the file table and event queue, with ident ID,
+        * starting at sector START and ending just before sector END.
+        */
 {
   struct file *f;
   size_t i;
@@ -163,26 +379,42 @@ static void put_file(ident id, secaddr start, secaddr end)
 }
 
 static void put_menu(dvd_reader_t *dvd, unsigned title)
+       /* Add the menu file for the given TITLE number to the file table and
+        * event queue; use the reader DVD to find out which sectors it
+        * occupies, if it even exists.
+        */
 {
   ident id = mkident(VOB, title, 0);
   char fn[MAXFNSZ];
   secaddr start, len;
 
+  /* Find out where the file is. */
   store_filename(fn, id);
   start = UDFFindFile(dvd, fn, &len); if (!start) return;
+
 #ifdef DEBUG
+  /* Print out what we've discovered. */
   printf(";; %8"PRIuSEC" .. %-8"PRIuSEC": %s\n",
         start, start + SECTORS(len), fn);
 #endif
+
+  /* Register the file and boundary events. */
   put_file(id, start, start + SECTORS(len));
 }
 
 static void put_title(dvd_reader_t *dvd, unsigned title)
+       /* Add the titleset file for the given TITLE number to the file table
+        * and event queue; use the reader DVD to find out which sectors it
+        * occupies, if it even exists.
+        */
 {
   char fn[MAXFNSZ];
   secaddr start[9], len[9];
   unsigned i, npart;
 
+  /* First step: find out where all of the parts of the titleset are.  I'm
+   * assuming that there aren't gaps in the numbering.
+   */
   for (i = 0; i < 9; i++) {
     store_filename(fn, mkident(VOB, title, i + 1));
     start[i] = UDFFindFile(dvd, fn, &len[i]); if (!start[i]) break;
@@ -190,6 +422,7 @@ static void put_title(dvd_reader_t *dvd, unsigned title)
   npart = i; if (!npart) return;
 
 #ifdef DEBUG
+  /* Print out what we've discovered. */
   for (i = 0; i < npart; i++) {
     store_filename(fn, mkident(VOB, title, i + 1));
     printf(";; %8"PRIuSEC" .. %-8"PRIuSEC": %s\n",
@@ -197,6 +430,11 @@ static void put_title(dvd_reader_t *dvd, unsigned title)
   }
 #endif
 
+  /* Second step: check that the parts all butt up against each other in the
+   * correct order.  For this to work, the lengths, which are expressed in
+   * /bytes/ by `UDFFindFile', of all but the last part must be a whole
+   * number of sectors.
+   */
   if (npart > 1)
     for (i = 0; i < npart - 1; i++) {
       if (len[i]%SECTORSZ)
@@ -208,35 +446,46 @@ static void put_title(dvd_reader_t *dvd, unsigned title)
           title, i, start[i] + len[i]/SECTORSZ, i + 1, start[i + 1]);
     }
 
+  /* All good: register a single file and its boundary events. */
   put_file(mkident(VOB, title, 1),
           start[0], start[npart - 1] + SECTORS(len[npart - 1]));
 }
 
-static dvd_reader_t *dvd;
-static int dvdfd = -1, outfd = -1;
-static struct file *file;
-static dvd_file_t *vob;
-static const char *mapfile; static FILE *mapfp;
-static const char *errfile; static FILE *errfp;
-static secaddr limit;
-static secaddr bad_start;
-static unsigned retry, max_retries = 4;
-
-static secaddr nsectors, ndone;
-static secaddr last_pos;
-static struct timeval last_time;
-static double alpha = 0.1;
-static double avg = 0.0, corr = 0.0;
-static int bad_err;
-
-static const char throbber[] = "|<-<|>->";
-static unsigned throbix = 0;
-
-static struct progress_item
+/*----- Common variables used by the copying machinery --------------------*/
+
+/* General reading state. */
+static dvd_reader_t *dvd;              /* `libdvdread' state for device */
+static int dvdfd = -1, outfd = -1;     /* input device and output image */
+static struct file *file;              /* currently active file */
+static dvd_file_t *vob;                        /* current `.VOB' file, or null */
+static const char *mapfile; static FILE *mapfp; /* skipped regions map */
+static const char *errfile; static FILE *errfp; /* bad-sector log */
+static secaddr limit;                  /* upper bound on sectors */
+
+static secaddr bad_start;              /* start of current bad region */
+static unsigned retry, max_retries = 4;        /* retry state */
+
+/*----- Progress reporting ------------------------------------------------*/
+
+static secaddr nsectors, ndone;                /* number of sectors done/to do */
+static secaddr last_pos;               /* position last time we updated */
+static struct timeval last_time;       /* time last time we updated */
+static double alpha = 0.1;             /* weighting factor for average */
+static double avg = 0.0, corr = 1.0;   /* exponential moving average */
+static int bad_err;                    /* most recent error code */
+
+static const char throbber[] = "|<-<|>->"; /* throbber pattern */
+static unsigned throbix = 0;           /* current throbber index */
+
+static struct progress_item            /* stock progress items */
   copy_progress, disc_progress,
   file_progress, badblock_progress;
 
 static double scale_bytes(double n, const char **unit_out)
+       /* Determine a human-readable representation for N bytes.  Divide N
+        * by some power of 1024, and store in *UNIT_OUT a string
+        * representing the conventional unit-prefix for that power of 1024.
+        */
 {
   const char *unit = "";
 
@@ -247,8 +496,12 @@ static double scale_bytes(double n, const char **unit_out)
   *unit_out = unit; return (n);
 }
 
-#define TIMESTRMAX 16
+#define TIMESTRMAX 16              /* maximum length of a duration string */
 static char *fmttime(unsigned long t, char *buf)
+       /* Format a count T of seconds.  Write a suitable string to BUF,
+        * which will be no longer than `TIMESTRMAX' bytes including the
+        * terminating zero.  Return BUF.
+        */
 {
   if (t < 60) sprintf(buf, "%ld s", t);
   else if (t < 3600) sprintf(buf, "%ld:%02ld", t/60, t%60);
@@ -257,14 +510,22 @@ static char *fmttime(unsigned long t, char *buf)
 }
 
 static void render_perfstats(struct progress_render_state *render)
+       /* Add performance statistics to RENDER.
+        *
+        * Specifically: the average transfer rate, and the estimated time to
+        * completion.  (See `update_progress' for how the average
+        * computation works.)
+        */
 {
   int eta;
   char timebuf[TIMESTRMAX];
   double rate;
   const char *unit;
 
+  /* If there's no average computed yet, then use some placeholder values. */
   rate = avg/(1 - corr); eta = (int)((nsectors - ndone)/rate + 0.5);
 
+  /* Write out the statistics. */
   rate = scale_bytes(rate*SECTORSZ, &unit);
   progress_putright(render, "ETA %s ", avg ? fmttime(eta, timebuf) : "???");
   progress_putright(render, "%.1f %sB/s, ", rate, unit);
@@ -272,6 +533,9 @@ static void render_perfstats(struct progress_render_state *render)
 
 static void render_copy_progress(struct progress_item *item,
                                 struct progress_render_state *render)
+       /* Render the progress for the copy, i.e., the number of sectors
+        * copied against the total number to be copied.
+        */
 {
   double frac = (double)ndone/nsectors;
 
@@ -285,6 +549,9 @@ static void render_copy_progress(struct progress_item *item,
 
 static void render_disc_progress(struct progress_item *item,
                                 struct progress_render_state *render)
+       /* Render the progress for the disc, i.e., the current position
+        * against the total number of sectors on the disc.
+        */
 {
   double frac = (double)last_pos/limit;
 
@@ -295,6 +562,9 @@ static void render_disc_progress(struct progress_item *item,
 
 static void render_file_progress(struct progress_item *item,
                                 struct progress_render_state *render)
+       /* Render the progress for the current file, i.e., the current
+        * position within the file against the file size.
+        */
 {
   secaddr off = last_pos - file->start, len = file->end - file->start;
   char fn[MAXFNSZ];
@@ -309,6 +579,9 @@ static void render_file_progress(struct progress_item *item,
 
 static void render_badblock_progress(struct progress_item *item,
                                     struct progress_render_state *render)
+       /* Render a notice about the progress through the current bad block
+        * region.
+        */
 {
   secaddr n = last_pos - bad_start;
   int bg;
@@ -330,43 +603,161 @@ static void render_badblock_progress(struct progress_item *item,
 }
 
 static void update_progress(secaddr pos)
+       /* Recompute the data displayed by the progress renderer functions
+        * above, based on the new current sector POS.
+        */
 {
   struct timeval now;
   double t, beta_t, rate;
 
+  /* We're using an exponential moving average with a weighting factor of Î±
+   * (`alpha', above); larger values are more sensitive to recent changes.
+   * If the old average was v_1, and the measurement in the current interval
+   * is x, then the new average after this interval is
+   *
+   *        v = Î± x + (1 âˆ’ Î±) v_1 .
+   *
+   * Write Î² = 1 âˆ’ Î±; so
+   *
+   *        v = Î± x + Î² v_1 .
+   *
+   * Let x_0 = x, let x_1 be the measurement from the previous interval, and,
+   * in general, let x_i be the measurement from i intervals ago.  Then
+   * another way to write the above would be
+   *
+   *        v = Î± (x_0 + Î² x_1 + â‹¯ + Î²^i x_i + â‹¯) .
+   *
+   * Alas, our time intervals are not regular.  Suppose that we get our next
+   * measurement after a gap of t intervals, for some integer t.  We can
+   * compensate approximately by pretending that all of the missed intervals
+   * -- and our new one -- had the same mean rate.  Then we'd have
+   * calculated
+   *
+   *        v = Î± (x + Î² x + â‹¯ + Î²^{t−1} x) + Î²^t v_1
+   *
+   *                1 âˆ’ Î²^t
+   *          = Î± x ------- + Î²^t v_1
+   *                 1 âˆ’ Î²
+   *
+   *          = x (1 âˆ’ Î²^t) + Î²^t v_1              (since Î± = 1 âˆ’ Î²)
+   *
+   *          = x + Î²^t (v_1 âˆ’ x) .
+   *
+   * Does this work in general?  It's clearly correct in the case t = 1.
+   *
+   * Suppose the old average was v_2, and that over a period of t intervals
+   * (where t is not necessarily an integer) we measured a mean rate of x,
+   * and then after u intervals we measured a mean rate of x /again/.  Then
+   * we'd firstly determine
+   *
+   *        v_1 = x + Î²^t (v_2 âˆ’ x)
+   *
+   * and then
+   *
+   *        v = x + Î²^u (v_1 âˆ’ x)
+   *
+   *          = x + Î²^u (x + Î²^t (v_2 âˆ’ x) âˆ’ x)
+   *
+   *          = x + Î²^{t+u} (v_2 âˆ’ x) ,
+   *
+   * which is exactly what we'd have done if we'd calculated the same mean
+   * rate over the combined span of t + u intervals.
+   *
+   * One final wrinkle, in case that wasn't enough.  There's a problem with
+   * the initial setup of an exponential moving average.  Apparently
+   * (https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
+   * explains that we can do this better by calculating the average after k
+   * intervals as
+   *
+   *             x_0 + Î² x_1 + Î²^2 x_2 + â‹¯ + Î²^{k−1} x_{k−1}
+   *        v′ = ------------------------------------------- .
+   *                      1 + Î² + Î²^2 + â‹¯ + Î²^{k−1}
+   *
+   * The numerator is our existing v/α; the denominator is (1 âˆ’ Î²^k)/α; the
+   * factors of Î± cancel, and we find that v′ = v/(1 âˆ’ Î²^k).  This still
+   * holds in our situation, where k may not be an integer.
+   *
+   * To apply all of this:
+   *
+   *   * we maintain the moving average v in `avg';
+   *
+   *   * we maintain the total Î²^k in `corr'; and
+   *
+   *   * we compute v′ = v/(1 âˆ’ Î²^k) on demand up in `render_perfstats'.
+   */
+
+  /* Find the current time and the delta since the last time we updated.
+   * This will be the length of the current interval.
+   */
   gettimeofday(&now, 0); t = tvdiff(&last_time, &now);
+
+  /* If no time at all has passed (unlikely!) then skip the rate
+   * calculation.  (The moving average wouldn't be affected anyway.)
+   */
   if (t) {
+    /* Update the moving average and the correction term, and start the next
+     * interval.
+     */
+
     rate = (pos - last_pos)/t; beta_t = pow(1 - alpha, t);
     avg = rate + beta_t*(avg - rate); corr *= beta_t;
     ndone += pos - last_pos; last_time = now; last_pos = pos;
   }
+
+  /* Advance the throbber character. */
   throbix++; if (!throbber[throbix]) throbix = 0;
 }
 
 static void report_progress(secaddr pos)
+       /* Update the progress variables (as `update_progress') and redraw
+        * the progress display.
+        */
   { update_progress(pos); progress_update(&progress); }
 
+/*----- Basic disc I/O ----------------------------------------------------*/
+
 struct badblock { secaddr start, end; };
 DEFVEC(badblock_v, struct badblock);
 static badblock_v badblocks = VEC_INIT;
+       /* This is a list of /fake/ bad-block ranges, used to test the
+        * recovery algorithm.  It's a rule that the ranges in this table
+        * mustn't overlap -- though it's OK if they abut.
+        */
 
 static int compare_badblock(const void *a, const void *b)
+       /* A `qsort' comparison function for the fake bad-blocks list.
+        * Ranges which start earlier are sorted before rangers which start
+        * later.
+        */
 {
   const struct badblock *ba = a, *bb = b;
 
+  /* Order by start sector. */
   if (ba->start < bb->start) return (-1);
   else if (ba->start > bb->start) return (+1);
 
+  /* Order by end sector as a tiebreak.  This shouldn't be possible. */
   if (ba->end < bb->end) return (-1);
   else if (ba->end > bb->end) return (+1);
 
+  /* They're equal.  This shouldn't be possible either. */
   return (0);
 }
 
-static double bad_block_delay = 0.0;
-static double good_block_delay = 0.0;
+static double bad_block_delay = 0.0, good_block_delay = 0.0;
+                              /* delay parameters for performance testing */
 
 static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
+       /* Try to read WANT sectors from the input, starting with sector POS,
+        * and write the contents to BUF.  Return the number of /whole
+        * sectors/ read; this will be 0 at end-of-file (though that
+        * shouldn't happen).  The returned length will be smaller than WANT
+        * only if end-of-file or a system error prevents reading further.
+        * Returns -1 on a system error if that prevented us from reading
+        * anything at all.
+        *
+        * This function is where the fake bad-blocks list is handled.
+        */
 {
   ssize_t n, done;
   size_t lo, mid, hi;
@@ -374,7 +765,22 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
   struct badblock *bad, *best;
   unsigned char *p = buf;
 
+  /* See whether the requested range intersects a bad-blocks range. */
   if (badblocks.n) {
+    /* Since the list is sorted, we use a binary search.  We're looking for
+     * the earliest-starting range which /ends after/ POS.  If this starts
+     * /at or before/ POS, then POS itself is a bad sector, and we should
+     * pretend an I/O error; otherwise, if the bad range /starts/ somewhere
+     * in the range we're trying to read then we must pretend a short read;
+     * and otherwise there's nothing to do.
+     */
+
+    /* Throughout, `best' points to the earliest-starting range we've found
+     * which (starts and) finishes after POS.  Ranges with indices below LO
+     * end too early to be interesting; similarly, ranges with indices HI or
+     * above start later than POS.  If we find a range which actually covers
+     * POS exactly then we'll stop early.
+     */
     best = 0; lo = 0; hi = badblocks.n;
 #ifdef DEBUG
     progress_clear(&progress);
@@ -382,11 +788,26 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
           pos, pos + want);
 #endif
     while (lo < hi) {
+      /* Standard binary-search loop: we continue until the pointers
+       * converge.
+       */
+
+      /* Try the midpoint between the two bounds. */
       mid = lo + (hi - lo)/2; bad = &badblocks.v[mid];
 #ifdef DEBUG
       printf(";;   try %zu (%"PRIuSEC" .. %"PRIuSEC")... ",
             mid, bad->start, bad->end);
 #endif
+
+      /* Follow our invariant.  If the range starts strictly after POS, then
+       * it's too late to overlap, so bring down HI to cover it; but it must
+       * be closer than any previous block we've found, so remember it in
+       * `best'.  Similarly, if the range ends /at or before/ POS then it
+       * stops too early, so bring up LO to cover it (but otherwise forget
+       * about it because it can't affect what we're doing).
+       *
+       * If we get a match then we stop immediately and fake a bad block.
+       */
       if (pos < bad->start) { D( printf("high\n"); ) best = bad; hi = mid; }
       else if (pos >= bad->end) { D( printf("low\n"); ) lo = mid + 1; }
       else {
@@ -394,6 +815,11 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
        errno = EIO; sit(bad_block_delay); return (-1);
       }
     }
+
+    /* We're done.  Check to see whether the bad range starts early enough.
+     * If so, remember that we're simulating an error, apply the delay, and
+     * bamboozle the rest of the code into performing a short read.
+     */
 #ifdef DEBUG
     if (best)
       printf(";;   next is %"PRIuSEC" .. %"PRIuSEC"\n",
@@ -403,8 +829,19 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
       { want = best->start - pos; fakeerr = EIO; sit(bad_block_delay); }
   }
 
+  /* Try to read stuff into the buffer until we find a reason why we can't
+   * continue.  Obviously we need to keep track of how much stuff we've read
+   * on previous iterations.
+   */
   done = 0; errno = 0;
   while (want) {
+
+    /* Read from the current file's input source.  If that's a scrambled
+     * video file, then use `libdvdread'; if it's the `raw' file, then go to
+     * the block device; if it's nothing at all, then fill with zeros.
+     * Always force a seek to the right place, in case things got messed up
+     * by some previous error.
+     */
     if (vob)
       { errno = 0; n = DVDReadBlocks(vob, pos - file->start, want, p); }
     else if (file) {
@@ -417,6 +854,11 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
       n = want;
     }
 
+    /* If we read some stuff then update the buffer pointer and lengths.  If
+     * we hit end-of-file then stop.  If we hit a bad sector then maybe make
+     * a note of it in the bad-sector log.  On any other kind of error, just
+     * stop.
+     */
     if (n > 0) { done += n; pos += n; p += n*SECTORSZ; want -= n; }
     else if (!n) break;
     else if (errno == EIO && errfile) {
@@ -426,19 +868,68 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
       break;
     } else if (errno != EINTR) break;
   }
+
+  /* We made it.  If we saved up a fake error, and there wasn't a real error
+   * (which should obviously take priority) then present the fake error to
+   * the caller.  If there wasn't an error, then everything must have been
+   * good so impose the good-block delay -- note that a bad-block delay will
+   * already have been imposed above.  Finally, return the accumulated count
+   * of sectors successfully read, or report the end-of-file or error
+   * condition as applicable.
+   */
   if (fakeerr && !errno) errno = fakeerr;
   else if (done > 0 && good_block_delay) sit(done*good_block_delay);
   return (!done && errno ? -1 : done);
 }
 
+/*----- Tracking machinery for the bad-sector algorithm -------------------*
+ *
+ * While we're probing around trying to find the end of the bad region, we'll
+ * have read some good data.  We want to try to keep as much good data as we
+ * can, and avoid re-reading it because (a) it's pointless I/O work, but more
+ * importantly (b) it might not work the second time.  The machinery here
+ * is for making this work properly.
+ *
+ * There are two parts to this which don't really intersect, but for
+ * convenience the tracking information for them is kept in the same
+ * `recoverybuf' structure.
+ *
+ *   * The `short-range' machinery keeps track of a contiguous region of good
+ *     data stored in the caller's buffer.
+ *
+ *   * The `long-range' machinery keeps track of a contiguous region of good
+ *     data that's beyond the range of the buffer.
+ */
+
 struct recoverybuf {
-  unsigned char *buf;
-  secaddr sz, pos, start, end;
-  secaddr good_lo, good_hi;
+       /* Information used to keep track of where good and bad sectors are
+        * while we're trying to find the end of a region of bad sectors.
+        */
+
+  /* Short-range buffer tracking. */
+  unsigned char *buf;                  /* pointer to the actual buffer */
+  secaddr sz;                          /* size of the buffer in sectors */
+  secaddr pos;                         /* sector address corresponding to
+                                        *   the start of the buffer */
+  secaddr start, end;                  /* bounds of the live region within
+                                        *   the buffer, as offsets in
+                                        *   sectors from the buffer start */
+
+  /* Long-range tracking. */
+  secaddr good_lo, good_hi;            /* known-good region, as absolute
+                                        *   sector addresses */
 };
 
 static void rearrange_sectors(struct recoverybuf *r,
                              secaddr dest, secaddr src, secaddr len)
+       /* Shuffle data about in R's buffer.  Specifically, move LEN sectors
+        * starting SRC sectors from the start of the buffer to a new
+        * position DEST sectors from the start.
+        *
+        * Unsurprisingly, this is a trivial wrapper around `memmove', with
+        * some range checking thrown in; it's only used by `recovery_read_-
+        * buffer' and `find_good_sector' below.
+        */
 {
   assert(dest + len <= r->sz); assert(src + len <= r->sz);
   memmove(r->buf + dest*SECTORSZ, r->buf + src*SECTORSZ, len*SECTORSZ);
@@ -448,6 +939,7 @@ static void rearrange_sectors(struct recoverybuf *r,
 static PRINTF_LIKE(2, 3)
   void show_recovery_buffer_map(const struct recoverybuf *r,
                                const char *what, ...)
+       /* Dump a simple visualization of the short-range tracking state. */
 {
   va_list ap;
 
@@ -470,6 +962,15 @@ static PRINTF_LIKE(2, 3)
 
 static ssize_t recovery_read_sectors(struct recoverybuf *r,
                                     secaddr pos, secaddr off, secaddr want)
+       /* Try to read WANT sectors starting at sector address POS from the
+        * current file into R's buffer, at offset OFF sectors from the start
+        * of the buffer.  Return the number of sectors read, zero if at end
+        * of file, or -1 in the event of a system error.
+        *
+        * This is a trivial wrapper around `read_sectors' with some
+        * additional range checking, used only by `recovery_read_buffer'
+        * below.
+        */
 {
   ssize_t n;
 
@@ -481,23 +982,54 @@ static ssize_t recovery_read_sectors(struct recoverybuf *r,
 
 static ssize_t recovery_read_buffer(struct recoverybuf *r,
                                    secaddr pos, secaddr want)
+       /* Try to read WANT sectors, starting at sector address POS, from the
+        * current file into the buffer R, returning a count of the number of
+        * sectors read, or 0 if at end of file, or -1 in the case of a
+        * system error, as for `read_sectors'.  The data will end up
+        * /somewhere/ in the buffer, but not necessarily at the start.
+        */
 {
   secaddr diff, pp, nn;
   ssize_t n;
 
+  /* This is the main piece of the short-range tracking machinery.  It's
+   * rather complicated, so hold on tight.  (It's much simpler -- and less
+   * broken -- than earlier versions were, though.)
+   */
+
 #ifdef DEBUG
   progress_clear(&progress);
   show_recovery_buffer_map(r, "begin(%"PRIuSEC", %"PRIuSEC")", pos, want);
 #endif
 
+  /* The first order of business is to make space in the buffer for this new
+   * data.  We therefore start with a case analysis.
+   */
   if (pos < r->pos) {
+    /* The new position is before the current start of the buffer, so we have
+     * no choice but to decrease the buffer position, which will involve
+     * shifting the existing material upwards.
+     */
+
+    /* Determine how far up we'll need to shift. */
     diff = r->pos - pos;
+
     if (r->start + diff >= r->sz) {
+      /* The material that's currently in the buffer would be completely
+       * shifted off the end, so we have no choice but to discard it
+       * completely.
+       */
+
       r->pos = pos; r->start = r->end = 0;
 #ifdef DEBUG
       show_recovery_buffer_map(r, "cleared; shift up by %"PRIuSEC"", diff);
 #endif
     } else {
+      /* Some of the material in the buffer will still be there.  We might
+       * lose some stuff off the end: start by throwing that away, and then
+       * whatever's left can be moved easily.
+       */
+
       if (r->end + diff > r->sz) r->end = r->sz - diff;
       rearrange_sectors(r, r->start + diff, r->start, r->end - r->start);
       r->pos -= diff; r->start += diff; r->end += diff;
@@ -506,18 +1038,44 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
 #endif
     }
   } else if (pos > r->pos + r->end) {
+    /* The new position is strictly beyond the old region.  We /could/ maybe
+     * keep this material, but it turns out to be better not to.  To keep it,
+     * we'd have to also read the stuff that's in between the end of the old
+     * region and the start of the new one, and that might contain bad
+     * sectors which the caller is specifically trying to skip.  We just
+     * discard the entire region here so as not to subvert the caller's
+     * optimizations.
+     */
+
     r->pos = pos; r->start = r->end = 0;
 #ifdef DEBUG
     show_recovery_buffer_map(r, "cleared; beyond previous region");
 #endif
   } else if (pos + want > r->pos + r->sz) {
+    /* The requested range of sectors extends beyond the region currently
+     * covered by the buffer.  We must therefore increase the buffer position
+     * which will involve shifting the existing material downwards.
+     */
+
+    /* Determine how far down we'll need to shift. */
     diff = (pos + want) - (r->pos + r->sz);
+
     if (r->end <= diff) {
+      /* The material that's currently in the buffer would be completely
+       * shifted off the beginning, so we have no choice but to discard it
+       * completely.
+       */
+
       r->pos = pos; r->start = r->end = 0;
 #ifdef DEBUG
       show_recovery_buffer_map(r, "cleared; shift down by %"PRIuSEC"", diff);
 #endif
     } else {
+      /* Some of the material in the buffer will still be there.  We might
+       * lose some stuff off the beginning: start by throwing that away, and
+       * then whatever's left can be moved easily.
+       */
+
       if (r->start < diff) r->start = diff;
       rearrange_sectors(r, r->start - diff, r->start, r->end - r->start);
       r->pos += diff; r->start -= diff; r->end -= diff;
@@ -527,8 +1085,27 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
     }
   }
 
+  /* We now have space in the buffer in which to put the new material.
+   * However, the buffer already contains some stuff.  We may need to read
+   * some data from the input file into an area before the existing
+   * material, or into an area following the existing stuff, or both, or
+   * (possibly) neither.
+   */
+
   if (pos < r->pos + r->start) {
+    /* The requested position is before the current good material, so we'll
+     * need to read some stuff there.
+     */
+
+    /* Determine the place in the buffer where this data will be placed, and
+     * how long it will need to be.  Try to extend it all the way to the
+     * existing region even if this is more than the caller wants, because it
+     * will mean that we can join it onto the existing region rather than
+     * having to decide which of two disconnected parts to throw away.
+     */
     pp = pos - r->pos; nn = r->start - pp;
+
+    /* Read the data. */
 #ifdef DEBUG
     printf(";; read low (%"PRIuSEC"@%"PRIuSEC", %"PRIuSEC")", pos, pp, nn);
     fflush(stdout);
@@ -537,10 +1114,24 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
 #ifdef DEBUG
     printf(" -> %zd\n", n);
 #endif
+
+    /* See whether it worked. */
     if (n != nn) {
+      /* We didn't get everything we wanted. */
+
+      /* If we got more than the caller asked for then technically this is
+       * good; but there must be some problem lurking up ahead, and the
+       * caller will want to skip past that.  So we don't update the tracking
+       * information to reflect our new data; even though this /looks/ like a
+       * success, it isn't really.
+       */
       if (n >= 0 && n > want) n = want;
+
+      /* We're done. */
       goto end;
     }
+
+    /* Extend the region to include the new piece. */
     r->start = pp;
 #ifdef DEBUG
     show_recovery_buffer_map(r, "joined new region");
@@ -548,7 +1139,18 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
   }
 
   if (pos + want > r->pos + r->end) {
+    /* The requested region extends beyond the current region, so we'll need
+     * to read some stuff there.
+     */
+
+    /* Determine the place in the buffer where this data will be placed, and
+     * how long it will need to be.  Note that pos <= r->pos + r->end, so
+     * there won't be a gap between the old good region and the material
+     * we're trying to read.
+     */
     pp = r->end; nn = (pos + want) - (r->pos + r->end);
+
+    /* Read the data. */
 #ifdef DEBUG
     printf(";; read high (%"PRIuSEC"@%"PRIuSEC", %"PRIuSEC")",
           r->pos + pp, pp, nn);
@@ -558,7 +1160,11 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
 #ifdef DEBUG
     printf(" -> %zd\n", n);
 #endif
+
+    /* See whether it worked. */
     if (n > 0) {
+      /* We read something, so add it onto the existing region. */
+
       r->end += n;
 #ifdef DEBUG
       show_recovery_buffer_map(r, "joined new region");
@@ -566,11 +1172,19 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r,
     }
   }
 
+  /* Work out the return value to pass back to the caller.  The newly read
+   * material has been merged with the existing region (the case where we
+   * didn't manage to join the two together has been handled already), so we
+   * can easily work out how much stuff is available by looking at the
+   * tracking information.  It only remains to bound the region size by the
+   * requested length.
+   */
   n = r->pos + r->end - pos;
   if (!n && want) n = -1;
   else if (n > want) n = want;
 
 end:
+  /* Done. */
 #ifdef DEBUG
   show_recovery_buffer_map(r, "done; return %zd", n);
 #endif
@@ -579,63 +1193,143 @@ end:
 
 static ssize_t recovery_read_multiple(struct recoverybuf *r,
                                      secaddr pos, secaddr want)
+       /* Try to read WANT sectors, starting at sector address POS, from the
+        * current file, returning a count of the number of sectors read, or
+        * 0 if at end of file, or -1 in the case of a system error, as for
+        * `read_sectors'.  Some data might end up in R's buffer, but if WANT
+        * is larger than R->sz then a lot will be just thrown away.
+        *
+        * This is only used by `recovery_read' below.
+        */
 {
   ssize_t n;
   secaddr skip, want0 = want;
 
+  /* If the request is larger than the buffer, then we start at the /end/ and
+   * work backwards.  If we encounter a bad sector while we're doing this,
+   * then we report a short read as far as the bad sector: the idea is to
+   * find the /latest/ bad sector we can.  The caller will want to skip past
+   * the bad sector, so the fact that we implicitly lied about the earlier
+   * data as being `good' won't matter.
+   */
+
   while (want > r->sz) {
+    /* There's (strictly!) more than a buffer's worth.  Fill the buffer with
+     * stuff and reduce the requested size.
+     */
+
     skip = want - r->sz;
     n = recovery_read_buffer(r, pos + skip, r->sz);
+
+    /* If it failed, then we always return a positive result, because we're
+     * pretending we managed to read all of the (nonempty) preceding
+     * material.
+     */
     if (n < r->sz) return (skip + (n >= 0 ? n : 0));
+
+    /* Cross off a buffer's worth and go around again. */
     want -= r->sz;
   }
+
+  /* Read the last piece.  If it fails or comes up short, then we don't need
+   * to mess with the return code this time.
+   */
   n = recovery_read_buffer(r, pos, want);
   if (n < 0 || n < want) return (n);
+
+  /* It all worked.  Return the full original amount requested. */
   return (want0);
 }
 
 static ssize_t recovery_read(struct recoverybuf *r,
                             secaddr pos, secaddr want)
+       /* Try to read WANT sectors, starting at sector address POS, from the
+        * current file, returning a count of the number of
+        * sectors read, or 0 if at end of file, or -1 in the case of a
+        * system error, as for `read_sectors'.  Some data might end up in
+        * R's buffer, but if WANT is larger than R->sz then a lot will be
+        * just thrown away.
+        */
 {
-  secaddr lo = pos, hi = pos + want, span;
+  secaddr lo = pos, hi = pos + want, span; /* calculate the request bounds */
   ssize_t n;
 
+  /* This is the main piece of the long-range tracking machinery.
+   * Fortunately, it's much simpler than the short-range stuff that we've
+   * just dealt with.
+   */
+
   if (hi < r->good_lo || lo > r->good_hi) {
+    /* The requested region doesn't abut or overlap with the existing good
+     * region, so it's no good to us.  Just read the requested region; if it
+     * worked at all, then replace the current known-good region with the
+     * region that was successfully read.
+     */
+
     n = recovery_read_multiple(r, lo, hi - lo);
     if (n > 0) { r->good_lo = lo; r->good_hi = lo + n; }
     return (n);
   }
 
   if (hi > r->good_hi) {
+    /* The requested region ends later than the current known-good region.
+     * Read the missing piece.  We're doing this first so that we find later
+     * bad sectors.
+     */
+
     span = hi - r->good_hi;
     n = recovery_read_multiple(r, r->good_hi, span);
+
+    /* If we read anything at all, then extend the known-good region. */
     if (n > 0) r->good_hi += n;
+
+    /* If we didn't read everything we wanted, then report this as a short
+     * read (so including some nonempty portion of the known-good region).
+     */
     if (n < 0 || n < span) return (r->good_hi - lo);
   }
 
   if (lo < r->good_lo) {
+    /* The requested region begins earlier than the known-good region. */
+
     span = r->good_lo - lo;
     n = recovery_read_multiple(r, lo, span);
+
+    /* If we read everything we wanted, then extend the known-good region.
+     * Otherwise, we're better off keeping the stuff after the bad block.
+     */
     if (n == span) r->good_lo = lo;
     else return (n);
   }
 
+  /* Everything read OK, and we've extended the known-good region to cover
+   * the requested region.  So return an appropriate code by consulting the
+   * new known-good region.
+   */
   n = r->good_hi - pos; if (n > want) n = want;
   if (!n) { errno = EIO; n = -1; }
   return (n);
 }
 
-static double clear_factor = 1.5;
-static secaddr clear_min = 1, clear_max = SECLIMIT;
-static double step_factor = 2.0;
-static secaddr step_min = 1, step_max = 0;
+/*----- Skipping past regions of bad sectors ------------------------------*/
+
+static double clear_factor = 0.5;    /* proportion of clear sectors needed */
+static secaddr clear_min = 1, clear_max = SECLIMIT; /* absolute bounds */
+static double step_factor = 2.0;       /* factor for how far to look ahead */
+static secaddr step_min = 1, step_max = 0; /* and absolute bounds */
 
 static void recovered(secaddr bad_lo, secaddr bad_hi)
+       /* Do all of the things that are necessary when a region of bad
+        * sectors has been found between BAD_LO (inclusive) and BAD_HI
+        * (exclusive).
+        */
 {
   char fn[MAXFNSZ];
 
+  /* Remove the progress display temporarily. */
   progress_clear(&progress);
 
+  /* Print a message into the permanent output log. */
   if (!file || id_kind(file->id) == RAW)
     moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC")",
         bad_hi - bad_lo, bad_lo, bad_hi);
@@ -649,61 +1343,129 @@ static void recovered(secaddr bad_lo, secaddr bad_hi)
   }
 
   if (mapfile) {
+    /* The user requested a map of the skipped regions, so write an entry. */
+
+    /* Open the file, if it's not open already. */
     open_file_on_demand(mapfile, &mapfp, "bad-sector region map");
+
+    /* Write the sector range. */
     fprintf(mapfp, "%"PRIuSEC" %"PRIuSEC" # %"PRIuSEC" sectors",
            bad_lo, bad_hi, bad_hi - bad_lo);
 
+    /* If we're currently reading from a file then note down the position in
+     * the file in the comment.  (Intentional bad sectors are frequently at
+     * the start and end of titles, so this helps a reader to decide how
+     * concerned to be.)
+     */
     if (file && id_kind(file->id) != RAW)
       fprintf(mapfp, "; `%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC"",
              fn, bad_lo - file->start, bad_hi - file->start,
              file->end - file->start);
 
+    /* Done.  Flush the output to the file so that we don't lose it if we
+     * crash!
+     */
     fputc('\n', mapfp);
     check_write(mapfp, "bad-sector region map");
   }
 
+  /* Adjust the position in our output file to skip past the bad region.
+   * (This avoids overwriting anything that was there already, which is
+   * almost certainly less wrong than anything we could come up with here.)
+   */
   if (lseek(outfd, (off_t)(bad_hi - bad_lo)*SECTORSZ, SEEK_CUR) < 0)
     bail_syserr(errno, "failed to seek past bad sectors");
 
+  /* Remove our notice now that we're no longer messing about with bad
+   * sectors, and reinstate the progress display.
+   */
   progress_removeitem(&progress, &badblock_progress);
   progress_update(&progress);
 }
 
 static secaddr run_length_wanted(secaddr pos, secaddr badlen, secaddr end)
+       /* Return the number of good sectors that we want to see before
+        * we're happy, given that we're about to try to read sector POS,
+        * which is BADLEN sectors beyond where we found the first bad
+        * sector, and the current region ends at sector END (i.e., this is
+        * where the next event occurs).
+        */
 {
   secaddr want;
 
+  /* Apply the factor to BADLEN to get an initial length. */
   want = ceil(clear_factor*badlen);
+
+  /* Apply the user-configurable lower bound. */
   if (want < clear_min) want = clear_min;
+
+  /* Cap this with the end of the region. */
   if (want > end - pos) want = end - pos;
+
+  /* And apply the user-configurable upper bound. */
   if (clear_max && want > clear_max) want = clear_max;
+
+  /* We're done. */
   return (want);
 }
 
 static void report_bad_blocks_progress(secaddr bad_hi, int err)
+       /* Report progress while we're trying to work past a region of bad
+        * sectors.  We're about to investigate BAD_HI, and the most recent
+        * error was ERR.
+        */
   { bad_err = err; report_progress(bad_hi); }
 
 static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
                                unsigned char *buf, secaddr sz)
+       /* Work out a place to resume after finding a bad sector.  The
+        * current position, where we found a problem, is in *POS_INOUT.  The
+        * current input region goes up up sector END (i.e., this is where
+        * the next event occurs).  The caller's buffer is at BUF, and can
+        * hold SZ sectors.  On exit, update *POS_INOUT to be the start of a
+        * region of /good/ sector that we decided was worth exploring, and
+        * return the number of sectors we've already read at that position
+        * and left at the start of the buffer.  (This number may be zero,
+        * depending on how things work out.  That doesn't mean that we hit
+        * end-of-file.)
+        *
+        * Altough the return value is `ssize_t', this is only to fit in with
+        * other read functions; a negative return is not actually possible.
+        */
 {
   secaddr pos = *pos_inout, bad_lo, bad_hi, good, step, want;
   struct recoverybuf r;
   ssize_t n;
 
+  /* Initial setup.  Save the initial state and establish the bad-blocks
+   * progress notice.
+   */
   bad_start = pos; bad_err = errno;
   badblock_progress.render = render_badblock_progress;
   progress_additem(&progress, &badblock_progress);
 
+  /* First, retry the `bad' sector a few times.  Sometimes, with damaged
+   * discs, this actually works.  We'll try to read a full buffer, but we're
+   * not expecting much.
+   */
   want = sz; if (want > end - pos) want = end - pos;
   for (retry = 0; retry < max_retries; retry++) {
+
+    /* Show the progress report. */
     report_bad_blocks_progress(pos, errno);
+
+    /* Try reading stuff. */
     n = read_sectors(pos, buf, want);
 #ifdef DEBUG
     progress_clear(&progress);
     printf(";; [retry] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
           pos, pos + want, n);
 #endif
+
     if (n > 0) {
+      /* We won!  Remove the progress display, and leave a permanent message
+       * to inform the user what happened.
+       */
       progress_clear(&progress);
       moan("sector %"PRIuSEC" read ok after retry", pos);
       progress_removeitem(&progress, &badblock_progress);
@@ -712,10 +1474,18 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
     }
   }
 
+  /* We're going to have to be more creative.  Set up the tracking state. */
   r.buf = buf; r.sz = sz; r.pos = r.start = r.end = 0;
   r.good_lo = r.good_hi = 0;
 
+  /* Set up the region bound.  We know the bad area starts at POS, and that
+   * it covers at least one sector.
+   */
   bad_lo = pos; bad_hi = pos + 1;
+
+  /* Second major step: try to find somewhere on the other side of the bad
+   * region.
+   */
   for (;;) {
 #ifdef DEBUG
     progress_clear(&progress);
@@ -723,29 +1493,58 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
           "%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
           bad_lo, bad_hi - bad_lo, bad_hi);
 #endif
+
+    /* If our upper bound has reached all the way to the end of the input
+     * region then there's nowhere to recover to.  Set the next position to
+     * the end of the region and return.
+     */
     if (bad_hi >= end) {
       progress_clear(&progress);
       moan("giving up on this extent");
       recovered(bad_lo, end); *pos_inout = end;
       return (0);
     }
+
+    /* Give a progress update. */
     report_bad_blocks_progress(bad_hi, errno);
+
+    /* Choose a new place to look.  Apply the step factor to the size of the
+     * current gap between the start and end of the bad region, and then
+     * bound by the user bounds and the input-region end.
+     *
+     * We make progress because `step' is at least 1: `step_min' is at least
+     * 1, and bad_hi < end or we'd have already bailed.
+     */
     step = (step_factor - 1)*(bad_hi - bad_lo);
     if (step < step_min) step = step_min;
     if (step_max && step > step_max) step = step_max;
     step += bad_hi - bad_lo;
     if (step > end - bad_lo) step = end - bad_lo;
+
+    /* Now we look at the last sector of the new interval we've just marked
+     * out.
+     */
+    pos = bad_lo + step - 1;
     want = run_length_wanted(pos, step, end);
     n = recovery_read(&r, pos, want);
 #ifdef DEBUG
     printf(";; [bound] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
           pos, pos + want, n);
 #endif
+
+    /* If everything went OK then we're done with this phase. */
     if (n == want) break;
+
+    /* If it failed then extend the bad region to cover (the end of) the bad
+     * sector which terminated the run, and go around again.
+     */
     if (n < 0) n = 0;
     bad_hi = pos + n + 1;
   }
 
+  /* Third major step: identify exactly where the bad region ends.  This is
+   * a binary search.
+   */
   good = pos;
   while (good > bad_hi) {
 #ifdef DEBUG
@@ -754,26 +1553,57 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
           "%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
           bad_lo, bad_hi - bad_lo, bad_hi, good - bad_hi, good);
 #endif
+
+    /* Update the progress report. */
     report_bad_blocks_progress(bad_hi, errno);
-    pos = bad_hi + (good - bad_hi)/2;
-    step = pos - bad_lo;
+
+    /* Pick a new place to try. */
+    pos = bad_hi + (good - bad_hi)/2; step = pos - bad_lo;
     want = run_length_wanted(pos, step, end);
+
+    /* Try reading. */
     n = recovery_read(&r, pos, want);
 #ifdef DEBUG
     printf(";; [limit] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
           pos, pos + want, n);
 #endif
+
+    /* If that worked -- i.e., we got all the data we wanted -- then bring
+     * down the `good' bound.  If it failed, then bring up `bad_hi' to cover
+     * the bad sector which terminated our read attempt.
+     */
     if (n < 0) n = 0;
     if (n == want) good = pos;
     else bad_hi = pos + n + 1;
   }
+
+  /* We're done.  It's time to tidy up.
+   *
+   * One subtle point: it's possible that, as a result of retrying previous
+   * bad blocks, that we ended up with bad_hi > good, so it's important that
+   * we make a consistent choice between the two.  I've gone with `good'
+   * because (a) this gives us more of the original data from the disc and
+   * (b) hopefully any marginal sectors are now in our buffer
+   */
   recovered(bad_lo, good); *pos_inout = good;
-  if (good < r.pos + r.start || r.pos + r.end <= good)
+
+  /* Figure out how much data we can return to the caller from our buffer. */
+  if (good < r.pos + r.start || r.pos + r.end <= good) {
+    /* Our new position is outside of the region covered by the short-range
+     * tracking, so there's nothing to return.
+     */
+
     n = 0;
-  else {
+  } else {
+    /* The new position is covered, so shuffle the data to the start of the
+     * buffer and return as much as we can.
+     */
+
     n = r.pos + r.end - good;
     rearrange_sectors(&r, 0, good - r.pos, n);
   }
+
+  /* We're done. */
 #ifdef DEBUG
   show_recovery_buffer_map(&r, "returning %zd good sectors at %"PRIuSEC"",
                           n, good);
@@ -781,9 +1611,15 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
   return (n);
 }
 
+/*----- Copying data from a single input file -----------------------------*/
+
 static void emit(secaddr start, secaddr end)
+       /* Copy sectors with absolute addresses from START (inclusive) to END
+        * (exclusive) to the output.  The entire input region comes from the
+        * same source, already established as `file'.
+        */
 {
-#define BUFSECTORS 512
+#define BUFSECTORS 512                 /* this is a megabyte */
 
   int least;
   unsigned char buf[BUFSECTORS*SECTORSZ];
@@ -798,11 +1634,19 @@ static void emit(secaddr start, secaddr end)
   int i;
 #endif
 
+  /* Choose an active file through which to read the source contents.  We're
+   * guaranteed that this file will do for the entire input region.  We
+   * choose the active file with the smallest index.  The virtual `raw' file
+   * which represents the underlying block device has the largest index, so
+   * we'll always use a `.VOB' file if one is available.  Looking at the
+   * protocol suggests that the host and drive identify the per-title CSS key
+   * by the start sector address of the `.VOB' file, so coincident files must
+   * all use the same key.  I've not encountered properly overlapping files
+   * in the wild.
+   */
   least = least_live();
-
 #ifdef DEBUG
   printf(";; %8"PRIuSEC" .. %"PRIuSEC"\n", start, end);
-
   for (i = 0; i < filetab.n; i++) {
     if (!livep(i)) continue;
     if (act == -1) act = i;
@@ -814,16 +1658,43 @@ static void emit(secaddr start, secaddr end)
   assert(act == least);
 #endif
 
-  if (least == -1)
-    { file = 0; vob = 0; }
-  else {
+  /* Set the global variables up for reading from the file we decided on.
+   * These will be primarily used by `read_sectors' and `update_progress'.
+   */
+  if (least == -1) {
+    /* There's nothing at all.  This can happen because the kernel reported
+     * the wrong block-device size for some reason but the filesystem has
+     * identified files which start beyond the reported size, leaving a gap.
+     */
+
+    file = 0; vob = 0;
+  } else {
+    /* There's a (possibly) virtual file. */
+
     file = &filetab.v[least];
     switch (id_kind(file->id)) {
+
       case RAW:
+       /* It's the raw device.  Clear `vob' to prompt `read_sectors' to read
+        * directly from `dvdfd'.
+        */
+
        vob = 0;
        break;
+
       case VOB:
+       /* It's a `.VOB' file.  We read these through `libdvdread', which
+        * handles CSS unscrambling for us.
+        */
+
+       /* The first time we open a `.VOB' file, `libdvdread' wants to spray
+        * a bunch of information about how it's getting on cracking the
+        * title keys.  This will interfere with the progress display, so
+        * preemptively hide the display.
+        */
        if (first_time) { progress_clear(&progress); first_time = 0; }
+
+       /* Open the `.VOB' file. */
        vob = DVDOpenFile(dvd, id_title(file->id),
                          id_part(file->id)
                            ? DVD_READ_TITLE_VOBS
@@ -833,36 +1704,79 @@ static void emit(secaddr start, secaddr end)
               id_part(file->id) ? "title" : "menu",
               id_title(file->id));
        break;
+
       default:
+       /* Some other kind of thing; but there shouldn't be anything else in
+        * the file table, so there's a bug.
+        */
        abort();
+
     }
   }
 
+  /* If we're not reading from the raw device then add an additional progress
+   * bar for the current file.  This isn't completely pointless: having a
+   * ready visualization for whereabouts we are in a file is valuable when we
+   * encounter bad blocks, because regions of intentional bad blocks near the
+   * starts and and ends of VOBs are common on discs from annoying studios.
+   */
   if (file && id_kind(file->id) != RAW) {
     file_progress.render = render_file_progress;
     progress_additem(&progress, &file_progress);
   }
 
+  /* Put the progress display back, if we took it away, and show the file
+   * progress bar if we added one.
+   */
   progress_update(&progress);
+
+  /* Read the input region and copy it to the disc. */
   pos = start;
   while (pos < end) {
+
+    /* Decide how much we want.  Fill the buffer, unless there's not enough
+     * input left.
+     */
     want = end - pos; if (want > BUFSECTORS) want = BUFSECTORS;
+
+    /* Try to read the input. */
     n = read_sectors(pos, buf, want);
 
-    if (n <= 0) n = find_good_sector(&pos, end, buf, BUFSECTORS);
-    if (n > 0) { carefully_write(outfd, buf, n*SECTORSZ); pos += n; }
+    if (n <= 0) {
+      /* It didn't work.  Time to deploy the skipping-past-bad-blocks
+       * machinery we worked so hard on.  This will fill the buffer with
+       * stuff and return a new count of how much it read.
+       */
+
+      n = find_good_sector(&pos, end, buf, BUFSECTORS);
+    }
+    if (n > 0) {
+      /* We made some progress.  Write the stuff that we read to the output
+       * file and update the position.
+       */
+
+      carefully_write(outfd, buf, n*SECTORSZ); pos += n;
+    }
+
+    /* Report our new progress. */
     report_progress(pos);
   }
 
+  /* Close the `libdvdread' file, if we opened one. */
   if (vob) { DVDCloseFile(vob); vob = 0; }
 
+  /* If we added a per-file progress bar, then take it away again. */
   if (file && id_kind(file->id) != RAW)
     progress_removeitem(&progress, &file_progress);
+
+  /* Update the progress display to report our glorious success. */
   progress_update(&progress);
 
 #undef BUFSECTORS
 }
 
+/*----- Main program ------------------------------------------------------*/
+
 int main(int argc, char *argv[])
 {
   unsigned f = 0;
@@ -897,66 +1811,138 @@ int main(int argc, char *argv[])
 #define f_write 256u
 
   set_prog(argv[0]);
+
+  /* First up, handle the command-line options. */
   for (;;) {
     opt = getopt(argc, argv, "hB:E:FR:X:b:cir:s"); if (opt < 0) break;
     switch (opt) {
+
+      /* `-h': Help. */
       case 'h': usage(stderr); exit(0);
+
+      /* `-B PARAM=VALUE[,...]': Setting internal parameters. */
       case 'B':
+
+       /* Set up a cursor into the parameter string. */
        p = optarg;
+
 #define SKIP_PREFIX(s)                                                 \
        (STRNCMP(p, ==, s "=", sizeof(s)) && (p += sizeof(s), 1))
+       /* If the text at P matches `S=' then advance P past that and
+        * evaluate nonzero; otherwise evaluate zero.
+        */
+
        for (;;) {
+
          if (SKIP_PREFIX("cf"))
            clear_factor = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
                                       "clear factor");
+
          else if (SKIP_PREFIX("cmin"))
            clear_min = parse_int(&p, PNF_JUNK, 1, SECLIMIT,
                                  "clear minimum");
+
          else if (SKIP_PREFIX("cmax"))
            clear_max = parse_int(&p, PNF_JUNK, 1, SECLIMIT,
                                  "clear maximum");
+
          else if (SKIP_PREFIX("sf"))
            step_factor = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
                                      "step factor");
+
          else if (SKIP_PREFIX("smin"))
            step_min = parse_int(&p, PNF_JUNK, 1, SECLIMIT - 1,
                                 "step minimum");
+
          else if (SKIP_PREFIX("smax"))
            step_max = parse_int(&p, PNF_JUNK, 1, SECLIMIT - 1,
                                 "step maximum");
+
          else if (SKIP_PREFIX("retry"))
            max_retries = parse_int(&p, PNF_JUNK, 0, INT_MAX, "retries");
+
          else if (SKIP_PREFIX("alpha"))
            alpha = parse_float(&p, PNF_JUNK, 0, 1, "average decay factor");
+
          else if (SKIP_PREFIX("_badwait"))
            bad_block_delay = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
                                          "bad-block delay");
+
          else if (SKIP_PREFIX("_blkwait"))
            good_block_delay = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
                                           "good block delay");
+
          else
            bail("unknown bad blocks parameter `%s'", p);
+
+         /* If we're now at the end of the string then we're done. */
          if (!*p) break;
+
+         /* We're not done yet, so there should now be a comma and another
+          * parameter setting.
+          */
          if (*p != ',') bail("unexpected junk in parameters");
          p++;
        }
+
 #undef SKIP_PREFIX
        break;
+
+      /* `-E FILE' (undocumented): Log the bad sectors we encountered to
+       * FILE.
+       */
       case 'E': errfile = optarg; break;
+
+      /* `-F' (undocumented): Hack for fixing up images that were broken by
+       * an old early-stop bug.
+       */
       case 'F': f |= f_fixup; break;
+
+      /* `-R FILE': Read ranges to retry from FILE.  Retry ranges are
+       * converted into `EV_WRITE' and `EV_STOP' events.
+       */
       case 'R':
        fp = fopen(optarg, "r");
        if (!fp)
          bail_syserr(errno, "failed to open ranges file `%s'", optarg);
+
+       /* We're going to try to coalesce adjacent ranges from the file.
+        * When we found a region to skip, we'd have stopped at the a file
+        * boundary, and possibly restarted again immediately afterwards,
+        * resulting in two adjacent regions in the file.  To do that, and
+        * also to police the restriction that ranges occur in ascending
+        * order, we keep track of the upper bound for the most recent range
+        * -- but there isn't one yet, so we use a sentinel value.
+        */
        i = 0; last = -1;
        for (;;) {
+
+         /* Read a line from the buffer. If there's nothing left then we're
+          * done.
+          */
          buf_rewind(&buf); if (read_line(fp, &buf)) break;
+
+         /* Increment the line counter and establish a cursor. */
          i++; p = buf.p;
+
+         /* Skip initial whitespace. */
          while (ISSPACE(*p)) p++;
+
+         /* If this is a comment then ignore it and go round again. */
          if (!*p || *p == '#') continue;
+
+         /* Parse the range.  Check that the ranges are coming out in
+          * ascending order.
+          */
          if (parse_range(p, 0, &start, &end) ||
              (last <= SECLIMIT && start < last))
            bail("bad range `%s' at `%s' line %zu", buf.p, optarg, i);
+
+         /* Ignore empty ranges: this is important (see below where we sort
+          * the event queue).  If this abuts the previous range then just
+          * overwrite the previous end position.  Otherwise, write a new
+          * pair of events.
+          */
          if (start < end) {
            if (start == last)
              eventq.v[eventq.n - 1].pos = end;
@@ -967,10 +1953,19 @@ int main(int argc, char *argv[])
            last = end;
          }
        }
+
+       /* Check for read errors. */
        if (ferror(fp))
          bail_syserr(errno, "failed to read ranges file `%s'", optarg);
        f |= f_retry;
        break;
+
+      /* `-X FILE' (undocumented): Read ranges of bad-blocks from FILE to
+       * establish fake bad blocks: see `read_sectors' above for the details.
+       *
+       * This is very similar to the `-R' option above, except that it
+       * doesn't do the range coalescing thing.
+       */
       case 'X':
        fp = fopen(optarg, "r");
        if (!fp)
@@ -992,30 +1987,49 @@ int main(int argc, char *argv[])
        if (ferror(fp))
          bail_syserr(errno, "failed to read bad-blocks file `%s'", optarg);
        break;
+
+      /* Log regions skipped because of bad blocks to a file. */
       case 'b':
        if (mapfile) bail("can't have multiple map files");
        mapfile = optarg;
        break;
+
+      /* `-c': Continue copying where we left off last time. */
       case 'c': f |= f_continue; break;
+
+      /* `-i': Check that we're copying from the right disc. */
       case 'i': f |= f_checkid; break;
+
+      /* `-r [START]-[END]': Manually provide a range of sectors to retry. */
       case 'r':
        start = 0; end = -1; f |= f_retry;
        if (parse_range(optarg, PRF_HYPHEN, &start, &end))
          bail("bad range `%s'", optarg);
        if (start < end) {
+         /* Again, ignore empty ranges. */
          put_event(EV_WRITE, 0, start);
          if (end <= SECLIMIT) put_event(EV_STOP, 0, end);
        }
        break;
+
+      /* `-s': Print statistics at the end. */
       case 's': f |= f_stats; break;
+
+      /* Anything else is an error. */
       default: f |= f_bogus; break;
     }
   }
+
+  /* We expect two arguments.  Check this.  Complain about bad usage if we
+   * have bad arguments or options.
+   */
   if (argc - optind != 2) f |= f_bogus;
   if (f&f_bogus) { usage(stderr); exit(2); }
-
   device = argv[optind]; outfile = argv[optind + 1];
 
+  /* If there are fake bad blocks (the `-X' option) then sort the list
+   * because `read_sectors' wants to use a binary search.
+   */
   if (badblocks.n) {
     qsort(badblocks.v, badblocks.n, sizeof(struct badblock),
          compare_badblock);
@@ -1027,10 +2041,16 @@ int main(int argc, char *argv[])
 #endif
   }
 
+  /* Prepare to display progress information. */
   setlocale(LC_ALL, "");
   progress_init(&progress);
+
+  /* Open the input device.  (This may pop up a notice if there's nothing in
+   * the drive.)
+   */
   if (open_dvd(device, O_RDONLY, &dvdfd, &dvd)) exit(2);
 
+  /* Determine the size of the input device and check the sector size. */
   blksz = SECTORSZ; volsz = device_size(dvdfd, device, &blksz);
   if (blksz != SECTORSZ)
     bail("device `%s' block size %d /= %d", device, blksz, SECTORSZ);
@@ -1038,6 +2058,10 @@ int main(int argc, char *argv[])
     bail("device `%s' volume size %"PRIu64" not a multiple of %d",
         device, volsz, SECTORSZ);
 
+  /* Maybe check that we're copying from the right disc.  This is intended to
+   * help avoid image corruption by from the wrong disc, but it obviously
+   * only works if the output file is mostly there.
+   */
   if (f&f_checkid) {
     if (open_dvd(outfile, O_RDONLY, 0, &dvd_out)) exit(2);
     if (dvd_id(id_in, dvd, DIF_MUSTIFOHASH, device) ||
@@ -1048,32 +2072,61 @@ int main(int argc, char *argv[])
           device, id_in, outfile, id_out);
   }
 
+  /* Open the output file. */
   outfd = open(outfile, O_WRONLY | O_CREAT, 0666);
   if (outfd < 0)
     bail_syserr(errno, "failed to create output file `%s'", outfile);
 
   if (f&f_continue) {
+    /* If we're continuing from where we left off, then find out where that
+     * was and make a note to copy from there to the end of the disc.  Note
+     * that we're not relying on this position: in particular, it might not
+     * even be sector-aligned (in which case we'll ignore the final partial
+     * sector).  We'll seek to the right place again when we start writing.
+     */
+
     off = lseek(outfd, 0, SEEK_END);
     if (off < 0)
       bail_syserr(errno, "failed to seek to end of output file `%s'",
                  outfile);
     put_event(EV_WRITE, 0, off/SECTORSZ); f |= f_retry;
   }
-  if (!(f&(f_retry | f_fixup)))
+
+  if (!(f&(f_retry | f_fixup))) {
+    /* If there are no ranges to retry and we're not fixing an ancient early-
+     * stop bug, then there's no range to retry and we should just copy
+     * everything.
+     */
+
     put_event(EV_WRITE, 0, 0);
+  }
 
-  /* It's fast enough just to check everything. */
+  /* Now it's time to figure out what the input looks like.  Work through the
+   * titlesets in order, mapping out where the video-object files are.  We
+   * could figure out how many there are properly, but it's fast enough just
+   * to try everything.  That's the menu only for the special titleset 0, and
+   * menu and titles for the remaining titlesets 1 up to 99.
+   */
   put_menu(dvd, 0);
   for (i = 1; i < 100; i++) {
     put_menu(dvd, i);
     put_title(dvd, i);
   }
+
+  /* Make a final virtual file for the raw device.  (See `emit', which
+   * assumes that this is the last entry in the file table.)  Check that we
+   * don't have more files than we expect, because the bitmap table has fixed
+   * size.
+   */
   put_file(mkident(RAW, 0, 0), 0, volsz/SECTORSZ);
   assert(filetab.n <= MAXFILES);
 
+  /* Find an upper limit for what we're supposed to copy.  Since the `RAW'
+   * entry covers the reported size of the input device, this ought to cover
+   * all of our bases.
+   */
   for (i = 0, limit = 0; i < filetab.n; i++)
     if (filetab.v[i].end > limit) limit = filetab.v[i].end;
-
 #ifdef DEBUG
   printf("\n;; files:\n");
   for (i = 0; i < filetab.n; i++) {
@@ -1084,42 +2137,106 @@ int main(int argc, char *argv[])
   }
 #endif
 
+  /* Sort the event list.
+   *
+   * The event-code ordering is important here.
+   *
+   *   * `EV_STOP' sorts /before/ `EV_WRITE'.  If we have two abutting ranges
+   *    to retry, then we should stop at the end of the first, and then
+   *    immediately start again.  If empty ranges were permitted then we'd
+   *    stop writing and /then/ start, continuing forever, which is clearly
+   *    wrong.
+   *
+   *   * `EV_BEGIN' sorts before `EV_END'.  If we have empty files then we
+   *    should set the bit that indicates that it's started, and then clear
+   *    it, in that order.  If we have abutting files, then we'll just both
+   *    bits for an instant, but that's not a problem.
+   */
   qsort(eventq.v, eventq.n, sizeof(struct event), compare_event);
 
+  /* Check that the event list is well-formed.  We start out at the
+   * beginning, not writing anything.
+   */
   for (i = 0, f &= ~f_write, start = 0; i < eventq.n; i++) {
     ev = &eventq.v[i];
     switch (ev->ev) {
+
       case EV_WRITE:
+       /* Start writing.  We shouldn't be writing yet! */
+
        if (f&f_write)
          bail("overlapping ranges: range from %"PRIuSEC" "
               "still open at %"PRIuSEC"",
               start, ev->pos);
        f |= f_write; start = ev->pos;
        break;
+
       case EV_STOP:
+       /* Stop writing.  Make a note that we've done this. */
+
        f &= ~f_write;
        break;
     }
   }
-
 #ifdef DEBUG
   dump_eventq("initial");
 #endif
+
+  /* Now we make a second pass over the event queue to fix it up.  Also
+   * count up how much work we'll be doing so that we can report progress.
+   */
   for (i = 0, f &= ~f_write, start = last = 0; i < eventq.n; i++) {
     ev = &eventq.v[i];
+
+    /* If we're supposed to start writing then make a note of the start
+     * position.  We'll want this to count up how much work we're doing.  The
+     * start position of the final range is also used by the logic below that
+     * determines the progress display.
+     */
     if (ev->ev == EV_WRITE) { start = ev->pos; f |= f_write; }
+
+    /* If this event position is past our final limit then stop.  Nothing
+     * beyond here can possibly be interesting.  (Since `EV_WRITE' sorts
+     * before other events, we will notice an `EV_WRITE' exactly at the limit
+     * sector, but not any other kind of event.)
+     */
     if (ev->pos >= limit) break;
+
+    /* If we're supposed to stop writing here, then add the size of the
+     * most recent range onto our running total.
+     */
     if (ev->ev == EV_STOP) { nsectors += ev->pos - start; f &= ~f_write; }
+
+    /* If we're fixing up images affected by the old early-stop bug, then
+     * remember this position.
+     */
     if (f&f_fixup) last = ev->pos;
   }
+
+  /* Truncate the event queue at the point we reached the sector limit. */
   eventq.n = i;
 #ifdef DEBUG
   dump_eventq("trimmed");
 #endif
+
+  /* Finally, the early-stop bug fix.
+   *
+   * The bug was caused by a broken version of the event-queue truncation
+   * logic: it trimmed the event queue, but didn't add a final event at the
+   * file limit.  The effect was that the interval between the last event --
+   * likely `EV_END' for a VOB file -- and the overall end of the disc didn't
+   * get copied.  We address this by starting to write at the position of
+   * this last event.
+   */
   if (f&f_fixup) {
     put_event(EV_WRITE, 0, last);
     f |= f_write;
   }
+
+  /* If we're still writing then avoid the early-end bug by adding an
+   * `EV_STOP' event at the limit position.  Include this range in the sector
+   * count.
+   */
   if (f&f_write) {
     nsectors += limit - start;
     put_event(EV_STOP, 0, limit);
@@ -1128,6 +2245,14 @@ int main(int argc, char *argv[])
   dump_eventq("final");
 #endif
 
+  /* Set up the main progress display.
+   *
+   * If we're copying a single region from somewhere to the end of the disc
+   * then it seems more sensible to use a single progress bar for both.  If
+   * we're reading multiple ranges, maybe because we're retrying bad blocks,
+   * then it's better to have separate bars for how much actual copying we've
+   * done, and which part of the disc we're currently working on.
+   */
   copy_progress.render = render_copy_progress;
   progress_additem(&progress, &copy_progress);
   if (nsectors == limit - start)
@@ -1137,23 +2262,50 @@ int main(int argc, char *argv[])
     progress_additem(&progress, &disc_progress);
   }
 
+  /* If we're producing overall statistics then make a note of the current
+   * time.
+   */
   if (f&f_stats) gettimeofday(&tv0, 0);
 
+  /* We're now ready to start our sweep through the disc. */
 #ifdef DEBUG
   printf("\n;; event sweep:\n");
 #endif
+
+  /* We start at the beginning of the disc, and the start of the event queue,
+   * not writing.  We'll advance through the events one by one.
+   */
   for (pos = 0, i = 0, f &= ~f_write; i < eventq.n; i++) {
+
+    /* Get the next event. */
     ev = &eventq.v[i];
+
+    /* If there's a nonempty range between here and the previous event then
+     * we need to process this.
+     */
     if (ev->pos > pos) {
+
+      /* If we're writing then copy the interval from the previous event to
+       * here to the output.
+       */
       if (f&f_write) emit(pos, ev->pos);
+
+      /* Advance the current position now that the output is up-to-date. */
       pos = ev->pos;
+
 #ifdef DEBUG
       progress_clear(&progress);
       printf(";;\n");
 #endif
     }
+
+    /* Decide what to action to take in response to the event. */
     switch (ev->ev) {
+
       case EV_BEGIN:
+       /* A file has started.  Set the appropriate bit in the active-files
+        * map.
+        */
        set_live(ev->file);
 #ifdef DEBUG
        store_filename(fn, filetab.v[ev->file].id);
@@ -1161,27 +2313,42 @@ int main(int argc, char *argv[])
        printf(";; %8"PRIuSEC": begin `%s'\n", pos, fn);
 #endif
        break;
+
       case EV_WRITE:
+       /* We're supposed to start writing. */
+
+       /* Note the current time and position for the progress display. */
        gettimeofday(&last_time, 0); last_pos = pos;
+
+       /* Seek to the right place in the output file. */
        if (lseek(outfd, (off_t)ev->pos*SECTORSZ, SEEK_SET) < 0)
          bail_syserr(errno,
                      "failed to seek to resume position "
                      "(sector %"PRIuSEC") in output file `%s'",
                      ev->pos, outfile);
+
+       /* Engage the write head. */
        f |= f_write;
+
 #ifdef DEBUG
        progress_clear(&progress);
        printf(";; %8"PRIuSEC": begin write\n", pos);
 #endif
        break;
+
       case EV_STOP:
+       /* We're supposed to stop writing.  Disengage the write head. */
+
        f &= ~f_write;
 #ifdef DEBUG
        progress_clear(&progress);
        printf(";; %8"PRIuSEC": end write\n", pos);
 #endif
        break;
+
       case EV_END:
+       /* We've found the end of a file.  Clear its bit in the table. */
+
        clear_live(ev->file);
 #ifdef DEBUG
        store_filename(fn, filetab.v[ev->file].id);
@@ -1189,15 +2356,20 @@ int main(int argc, char *argv[])
        printf(";; %8"PRIuSEC": end `%s'\n", pos, fn);
 #endif
        break;
+
+      /* Something else.  Clearly a bug. */
       default: abort();
     }
   }
 
+  /* Take down the progress display because we're done. */
   progress_clear(&progress);
 
+  /* Set the output file length correctly. */
   if (ftruncate(outfd, (off_t)limit*SECTORSZ) < 0)
     bail_syserr(errno, "failed to set output file `%s' length", outfile);
 
+  /* Report overall statistics. */
   if (f&f_stats) {
     gettimeofday(&tv1, 0); t = tvdiff(&tv0, &tv1);
     if (nsectors == limit) { ndone -= start; nsectors -= start; }
@@ -1207,6 +2379,7 @@ int main(int argc, char *argv[])
         tot, totunit, fmttime(t, timebuf), rate, rateunit);
   }
 
+  /* Close files. */
   if (dvd) DVDClose(dvd);
   if (dvdfd >= 0) close(dvdfd);
   if (outfd >= 0) close(outfd);
@@ -1214,6 +2387,7 @@ int main(int argc, char *argv[])
   carefully_fclose(errfp, "bad-sector error log");
   progress_free(&progress);
 
+  /* We're done! */
   return (0);
 
 #undef f_bogus
@@ -1222,3 +2396,5 @@ int main(int argc, char *argv[])
 #undef f_stats
 #undef f_write
 }
+
+/*----- That's all, folks -------------------------------------------------*/