]> git.decadent.org.uk Git - videolink.git/blobdiff - generate_dvd.cpp
Release versions 1.2.11 and 1.2.11-1
[videolink.git] / generate_dvd.cpp
index eb8fc0498fe0895cde21234810bfa5cc671773b6..371894d41edc23abd7ac34abee8337fd52b64711 100644 (file)
@@ -1,6 +1,7 @@
-// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
+// Copyright 2005-8 Ben Hutchings <ben@decadent.org.uk>.
 // See the file "COPYING" for licence details.
 
+#include <cassert>
 #include <cerrno>
 #include <cstring>
 #include <fstream>
 #include <sstream>
 #include <stdexcept>
 
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
 #include <gdkmm/pixbuf.h>
 #include <glibmm/miscutils.h>
 #include <glibmm/spawn.h>
@@ -88,8 +93,8 @@ namespace
     double menu_duration_seconds(const video::frame_params & params)
     {
        return double(menu_duration_frames(params))
-           * double(params.rate_numer)
-           / double(params.rate_denom);
+           / double(params.rate_numer)
+           * double(params.rate_denom);
     }
 
     void throw_length_error(const char * limit_type, std::size_t limit)
@@ -105,6 +110,17 @@ namespace
     // domain.  This seems to be an oddity of the parser that could be
     // fixed, but for now we'll have to work with it.
     const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
+
+    // The current navigation code packs menu and button number into a
+    // single register, so the number of menus is limited to
+    // dvd::reg_s8_button_mult - 1 == 1023.  However temp_file_name()
+    // is limited to 999 numbered files and it seems pointless to
+    // change it to get another 24.
+    // If people really need more we could use separate menu and
+    // button number registers, possibly allowing up to 11900 menus
+    // (the size of the indirect jump tables might become a problem
+    // though).
+    const unsigned menus_max = 999;
 }
 
 dvd_generator::dvd_generator(const video::frame_params & frame_params,
@@ -118,8 +134,8 @@ dvd_generator::pgc_ref dvd_generator::add_menu()
 {
     pgc_ref next_menu(menu_pgc, menus_.size());
 
-    if (next_menu.index == dvdauthor_anonymous_menus_max)
-       throw_length_error("number of menus", dvdauthor_anonymous_menus_max);
+    if (next_menu.index == menus_max)
+       throw_length_error("number of menus", menus_max);
 
     menus_.resize(next_menu.index + 1);
     return next_menu;
@@ -150,12 +166,12 @@ void dvd_generator::generate_menu_vob(unsigned index,
 
     std::string background_name(
        temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
-    std::cout << "saving " << background_name << std::endl;
+    std::cout << "INFO: Saving " << background_name << std::endl;
     background->save(background_name, "png");
 
     std::string highlights_name(
        temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
-    std::cout << "saving " << highlights_name << std::endl;
+    std::cout << "INFO: Saving " << highlights_name << std::endl;
     highlights->save(highlights_name, "png");
 
     std::string spumux_name(
@@ -200,11 +216,15 @@ void dvd_generator::generate_menu_vob(unsigned index,
                }
            }
        }
+       // Pad vertically to even y coordinates since dvdauthor claims
+       // odd values may result in incorrect display.
+       // XXX This may cause overlappping where it wasn't previously
+       // a problem.
        spumux_file << "      <button"
            " x0='" << this_entry.area.left << "'"
-           " y0='" << this_entry.area.top << "'"
+           " y0='" << (this_entry.area.top & ~1) << "'"
            " x1='" << this_entry.area.right << "'"
-           " y1='" << this_entry.area.bottom << "'"
+           " y1='" << ((this_entry.area.bottom + 1) & ~1) << "'"
            " left='" << (i == 0 ? button_count : i) << "'"
            " right='" << 1 + (i + 1) % button_count << "'"
            " up='" << 1 + up_button << "'"
@@ -219,68 +239,53 @@ void dvd_generator::generate_menu_vob(unsigned index,
     if (!spumux_file)
        throw std::runtime_error("Failed to write control file for spumux");
 
+    std::string output_name(
+       temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index));
+
     std::ostringstream command_stream;
-    unsigned frame_count(menu_duration_frames(frame_params_));
     if (encoder_ == mpeg_encoder_ffmpeg)
     {
-       for (unsigned i = 0; i != frame_count; ++i)
-       {
-           std::string frame_name(background_name);
-           frame_name.push_back('-');
-           frame_name.push_back('0' + i / 10);
-           frame_name.push_back('0' + i % 10);
-           if (symlink(background_name.c_str(), frame_name.c_str()) != 0)
-               throw std::runtime_error(
-                   std::string("symlink: ").append(std::strerror(errno)));
-       }
        command_stream <<
            "ffmpeg -f image2 -vcodec png"
            " -r " << frame_params_.rate_numer <<
            "/" << frame_params_.rate_denom <<
-           " -i " << background_name << "-%02d"
+           " -loop_input -i " << background_name <<
+           " -t " << menu_duration_seconds(frame_params_) <<
            " -target " << frame_params_.common_name <<  "-dvd"
-           " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
+           " -aspect 4:3 -an -y /dev/stdout";
     }
     else
     {
-       assert(encoder_ == mpeg_encoder_mjpegtools_old
-              || encoder_ == mpeg_encoder_mjpegtools_new);
+       assert(encoder_ == mpeg_encoder_mjpegtools);
        command_stream
            << "pngtopnm " << background_name
-           << " | ppmtoy4m -v0 -n" << frame_count << " -F"
-           << frame_params_.rate_numer << ":" << frame_params_.rate_denom
+           << " | ppmtoy4m -v0 -n" << menu_duration_frames(frame_params_)
+           << " -F" << frame_params_.rate_numer << ":" << frame_params_.rate_denom
            << " -A" << frame_params_.pixel_ratio_width
            << ":" << frame_params_.pixel_ratio_height
-           << " -Ip ";
-       // The chroma subsampling keywords changed between
-       // versions 1.6.2 and 1.8 of mjpegtools.  There is no
-       // keyword that works with both.
-       if (encoder_ == mpeg_encoder_mjpegtools_old)
-           command_stream << "-S420_mpeg2";
-       else
-           command_stream << "-S420mpeg2";
-       command_stream <<
+           << " -Ip -S420mpeg2"
            " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
            " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
     }
     command_stream
-       << " | spumux -v0 -mdvd " << spumux_name
-       << " > " << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index);
+       << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
     std::string command(command_stream.str());
     const char * argv[] = {
        "/bin/sh", "-c", command.c_str(), 0
     };
-    std::cout << "running " << command << std::endl;
+    std::cout << "INFO: Running " << command << std::endl;
     int command_result;
     Glib::spawn_sync(".",
                     Glib::ArrayHandle<std::string>(
                         argv, sizeof(argv)/sizeof(argv[0]),
                         Glib::OWNERSHIP_NONE),
                     Glib::SPAWN_STDOUT_TO_DEV_NULL,
-                    SigC::Slot0<void>(),
+                    sigc::slot<void>(),
                     0, 0,
                     &command_result);
-    if (command_result != 0)
+    struct stat stat_buf;
+    if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
+       || stat_buf.st_size == 0)
        throw std::runtime_error("spumux pipeline failed");
 }
 
@@ -299,257 +304,390 @@ dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
 
 void dvd_generator::generate(const std::string & output_dir) const
 {
+    // This function uses a mixture of 0-based and 1-based numbering,
+    // due to the differing conventions of the language and the DVD
+    // format.  Variable names ending in "_index" indicate 0-based
+    // indices and variable names ending in "_num" indicate 1-based
+    // numbers.
+
     std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
     std::ofstream file(name.c_str());
+    file << "<dvdauthor>\n";
 
     // We generate code that uses registers in the following way:
     //
-    // g0:     scratch
-    // g1:     target menu location
-    // g2:     source/return menu location for title
-    // g3:     target chapter number
-    //
-    // All locations are divided into two bitfields: the least
-    // significant 10 bits are a page/menu number and the most
-    // significant 6 bits are a link/button number, and numbering
-    // starts at 1, not 0.  This is done for compatibility with
-    // the encoding of the s8 (button) register.
-    //
-    static const int button_mult = dvd::reg_s8_button_mult;
-    static const int menu_mask = button_mult - 1;
-    static const int button_mask = (1 << dvd::reg_bits) - button_mult;
-
-    file <<
-       "<dvdauthor>\n"
-       "  <vmgm>\n"
-       "    <menus>\n";
-           
-    for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
+    // g0: Scratch.
+    // g1: Target location when jumping between menus.  Top 6 bits are
+    //     the button number (like s8) and bottom 10 bits are the menu
+    //     number.  This is used for selecting the appropriate button
+    //     when entering a menu, for completing indirect jumps between
+    //     domains, and for jumping to the correct menu after exiting a
+    //     title.  This is set to 0 in the pre-routine of the target
+    //     menu.
+    // g2: Current location in menus.  This is used for jumping to the
+    //     correct menu when the player exits a title.
+    // g3: Target chapter number plus 1 when jumping to a title.
+    //     This is used to jump to the correct chapter and to
+    //     distinguish between indirect jumps to menus and titles.
+    //     This is set to 0 in the pre-routine of the target title.
+    // g4: Source menu location used to jump to a title.  This is
+    //     compared with g2 to determine whether to increment the
+    //     button number if the title is played to the end.
+
+    static const unsigned button_mult = dvd::reg_s8_button_mult;
+    static const unsigned menu_mask = button_mult - 1;
+    static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
+
+    // Iterate over VMGM and titlesets.  For these purposes, we
+    // consider the VMGM to be titleset 0.
+
+    // We need a titleset for each title, and we may also need titlesets to
+    // hold extra menus if we have too many for the VMGM.
+    // Also, we need at least one titleset.
+    const unsigned titleset_end = std::max<unsigned>(
+           1U + std::max<unsigned>(1U, titles_.size()),
+           (menus_.size() + dvdauthor_anonymous_menus_max - 1)
+                / dvdauthor_anonymous_menus_max);
+
+    for (unsigned titleset_num = 0;
+        titleset_num != titleset_end;
+        ++titleset_num)
     {
-       const menu & this_menu = menus_[menu_index];
-
-       if (menu_index == 0)
+       const char * const outer_element_name =
+           titleset_num == 0 ? "vmgm" : "titleset";
+       const bool have_real_title =
+           titleset_num != 0 && titleset_num <= titles_.size();
+       const bool have_real_menus =
+           titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
+
+       file << "  <" << outer_element_name << ">\n" <<
+            "    <menus>\n"
+            "      <video format='" << frame_params_.common_name << "'/>\n";
+
+       const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
+       const unsigned menu_end =
+           have_real_menus
+           ? std::min<unsigned>(
+               (titleset_num + 1) * dvdauthor_anonymous_menus_max,
+               menus_.size())
+           : menu_begin + 1;
+
+       for (unsigned menu_index = menu_begin;
+            menu_index != menu_end;
+            ++menu_index)
        {
-           // This is the first (title) menu, displayed when the
-           // disc is first played.
-           file <<
-               "      <pgc entry='title'>\n"
-               "        <pre>\n"
-               // Set a default target location if none is set.
-               // This covers first play and use of the "top menu"
-               // button.
-               "          if (g1 eq 0)\n"
-               "            g1 = " << 1 + button_mult << ";\n";
-       }
-       else
-       {
-           file <<
-               "      <pgc>\n"
-               "        <pre>\n";
-       }
+           // There are various cases in which menus may be called:
+           //
+           // 1. The user follows a direct link to the menu.
+           // 2. The user follows an indirect link to some other menu
+           //    and that goes via this menu.  This is distinguished
+            //    from case 1 by the value of g1.  We must jump to or
+           //    at least toward the other menu.
+           // 3. The title menu is called when the disc is first
+           //    played or the user presses the "top menu" button.
+           //    This is distinguished from cases 2 and 3 by g1 == 0.
+           //    We make this look like case 1.
+           // 4. The root menu of a titleset is called when the user
+           //    follows an indirect link to the title.  This is
+           //    distinguished from all other cases by g3 != 0.  We
+           //    must jump to the title.
+           // 5. The root menu of a titleset is called when the title
+           //    ends or the user presses the "menu" button during
+           //    the title.  This is distinguished from cases 1, 2
+           //    and 4 by g1 == 0 and g3 == 0.  We must jump to the
+           //    latest menu (which can turn into case 1 or 2).
+           //
+           // Cases 3 and 5 do not apply to the same menus so they
+           // do not need to be distinguished.
+
+           if (menu_index == 0)
+           {
+               // Title menu.
+               file <<
+                   "      <pgc entry='title'>\n"
+                   "        <pre>\n"
+                   "          if (g1 eq 0)\n" // case 3
+                   "            g1 = " << 1 + button_mult << ";\n";
+           }
+           else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
+           {
+               // Root menu.
+               file <<
+                   "      <pgc entry='root'>\n"
+                   "        <pre>\n";
+               if (have_real_title)
+               {
+                   file <<
+                       "          if (g3 ne 0)\n" // case 4
+                       "            jump title 1;\n"
+                       "          if (g1 eq 0) {\n" // case 5
+                       "            g1 = g2;\n"
+                       "            jump vmgm menu entry title;\n"
+                       "          }\n";
+               }
+           }
+           else
+           {
+               // Some other menu.
+               file <<
+                   "      <pgc>\n"
+                   "        <pre>\n";
+           }
 
-       // When a title finishes or the user presses the "menu"
-       // button, this always jumps to the titleset's root menu.
-       // We want to return the user to the last menu they used.
-       // So we arrange for each titleset's root menu to return
-       // to the vmgm title menu and then dispatch from there to
-       // whatever the correct menu is.  We determine the correct
-       // menu by looking at the menu part of g1.
-
-       file << "          g0 = g1 &amp; " << menu_mask << ";\n";
-
-       // There is a limit of 128 VM instructions in each PGC.
-       // Therefore in each menu's <pre> section we generate
-       // jumps to menus with numbers greater by 512, 256, 128,
-       // ..., 1 where (a) such a menu exists, (b) this menu
-       // number is divisible by twice that increment and (c) the
-       // correct menu is that or a later menu.  Thus each menu
-       // has at most 10 such conditional jumps and is reachable
-       // by at most 10 jumps from the title menu.  This chain of
-       // jumps might take too long on some players; this has yet
-       // to be investigated.
-           
-       for (std::size_t menu_incr = (menu_mask + 1) / 2;
-            menu_incr != 0;
-            menu_incr /= 2)
-       {
-           if (menu_index + menu_incr < menus_.size()
-               && (menu_index & (menu_incr * 2 - 1)) == 0)
+           if (!have_real_menus)
            {
+               // This is a root menu only reachable from the title.
                file <<
-                   "          if (g0 ge " << 1 + menu_index + menu_incr
-                                          << ")\n"
-                   "            jump menu " << 1 + menu_index + menu_incr
-                                          << ";\n";
+                   "        </pre>\n"
+                   "      </pgc>\n";
+               continue;
            }
-       }
 
-       file <<
+           const menu & this_menu = menus_[menu_index];
+
+           // Detect and handle case 2.
+           //
+           // There is a limit of 128 VM instructions in each PGC.
+           // Also, we can't jump to an arbitrary menu in another
+           // domain.  Finally, we can't do computed jumps.
+           // Therefore we statically expand and distribute a binary
+           // search across the menus, resulting in a code size of
+           // O(log(menu_count)) in each menu.  In practice there are
+           // at most 11 conditional jumps needed in any menu.
+           //
+           // The initial bounds of the binary search are strange
+           // because we must ensure that any jump between titlesets
+           // is to the first menu of the titleset, marked as the
+           // root entry.
+
+           // Mask target location to get the target menu.
+           file << "          g0 = g1 &amp; " << menu_mask << ";\n";
+
+           for (unsigned
+                    bottom = 0,
+                    top = 16 * dvdauthor_anonymous_menus_max;
+                top - bottom > 1;)
+           {
+               unsigned middle = (bottom + top) / 2;
+               if (menu_index == bottom && middle < menus_.size())
+               {
+                   file << "          if (g0 ge " << 1 + middle << ")\n"
+                        << "            jump ";
+                   unsigned target_titleset_num =
+                       middle / dvdauthor_anonymous_menus_max;
+                   if (target_titleset_num != titleset_num)
+                   {
+                       assert(middle % dvdauthor_anonymous_menus_max == 0);
+                       file << "titleset " << target_titleset_num
+                            << " menu entry root";
+                   }
+                   else
+                   {
+                       file << "menu "
+                            << 1 + middle % dvdauthor_anonymous_menus_max;
+                   }
+                   file << ";\n";
+               }
+               if (menu_index >= middle)
+                   bottom = middle;
+               else
+                   top = middle;
+           }
+
+           // Case 1.
+
            // Highlight the appropriate button.
-           "          s8 = g1 &amp; " << button_mask << ";\n"
-           // Forget the link target.  If we don't do this, pressing
-           // the "top menu" button will result in jumping back to
-           // this same menu!
-           "          g1 = 0;\n"
-           "        </pre>\n"
-           "        <vob file='"
-            << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + menu_index)
-            << "'>\n"
-           // Define a cell covering the whole menu and set a still
-           // time at the end of that, since it seems all players
-           // support that but some ignore a still time set on a PGC.
-           "          <cell start='0' end='"
-            << std::fixed << std::setprecision(4)
-            << menu_duration_seconds(frame_params_) << "'"
-           " chapter='yes' pause='inf'/>\n"
-           "        </vob>\n";
-
-       for (unsigned button_index = 0;
-            button_index != this_menu.entries.size();
-            ++button_index)
-       {
-           const pgc_ref & target = this_menu.entries[button_index].target;
+           file << "          s8 = g1 &amp; " << button_mask << ";\n";
 
-           file << "        <button> ";
+           // Copy the target location to the current location and
+           // then clear the target location so that the title menu
+           // can distinguish cases 2 and 3.
+           file <<
+               "          g2 = g1;\n"
+               "          g1 = 0;\n";
 
-           if (target.type == menu_pgc)
+           file <<
+               "        </pre>\n"
+               "        <vob file='"
+                << temp_file_name(temp_dir_, "menu-%3d.mpeg",
+                                  1 + menu_index)
+                << "'>\n"
+               // Define a cell covering the whole menu and set a still
+               // time at the end of that, since it seems all players
+               // support that but some ignore a still time set on a PGC.
+               "          <cell start='0' end='"
+                << std::fixed << std::setprecision(4)
+                << menu_duration_seconds(frame_params_) << "'"
+               " chapter='yes' pause='inf'/>\n"
+               "        </vob>\n";
+
+           for (unsigned button_index = 0;
+                button_index != this_menu.entries.size();
+                ++button_index)
            {
-               unsigned target_button_num;
+               const pgc_ref & target =
+                   this_menu.entries[button_index].target;
 
-               if (target.sub_index)
+               file << "        <button> ";
+
+               if (target.type == menu_pgc)
                {
-                   target_button_num = target.sub_index;
+                   unsigned target_button_num;
+
+                   if (target.sub_index)
+                   {
+                       target_button_num = target.sub_index;
+                   }
+                   else
+                   {
+                       // Look for a button on the new menu that links
+                       // back to this one.  If there is one, set that to
+                       // be the highlighted button; otherwise, use the
+                       // first button.
+                       const std::vector<menu_entry> & target_menu_entries =
+                           menus_[target.index].entries;
+                       pgc_ref this_pgc(menu_pgc, menu_index);
+                       target_button_num = target_menu_entries.size();
+                       while (target_button_num != 1
+                              && (target_menu_entries[target_button_num - 1].target
+                                  != this_pgc))
+                           --target_button_num;
+                   }
+                        
+                   // Set new menu location.
+                   file << "g1 = "
+                        << (1 + target.index + target_button_num * button_mult)
+                        << "; ";
+                   // Jump to the target menu.
+                   unsigned target_titleset_num =
+                       target.index / dvdauthor_anonymous_menus_max;
+                   if (target_titleset_num == titleset_num)
+                       file << "jump menu "
+                            << 1 + (target.index
+                                    % dvdauthor_anonymous_menus_max)
+                            << "; ";
+                   else if (target_titleset_num == 0)
+                       file << "jump vmgm menu entry title; ";
+                   else
+                       file << "jump titleset " << target_titleset_num
+                            << " menu entry root; ";
                }
                else
                {
-                   // Look for a button on the new menu that links
-                   // back to this one.  If there is one, set that to
-                   // be the highlighted button; otherwise, use the
-                   // first button.
-                   const std::vector<menu_entry> & target_menu_entries =
-                       menus_[target.index].entries;
-                   pgc_ref this_pgc(menu_pgc, menu_index);
-                   target_button_num = target_menu_entries.size();
-                   while (target_button_num != 1
-                          && (target_menu_entries[target_button_num - 1].target
-                              != this_pgc))
-                       --target_button_num;
+                   assert(target.type == title_pgc);
+
+                   // Record current menu location and set target chapter
+                   // number.
+                   file <<
+                       "g2 = " << (1 + menu_index
+                                   + (1 + button_index) * button_mult) << "; "
+                       "g3 = " << 1 + target.sub_index << "; ";
+                   // Jump to the target title, possibly via its titleset's
+                   // root menu.
+                   unsigned target_titleset_num = 1 + target.index;
+                   if (titleset_num == 0)
+                       file << "jump title " << target_titleset_num << "; ";
+                   else if (target_titleset_num == titleset_num)
+                       file << "jump title 1; ";
+                   else
+                       file << "jump titleset " << target_titleset_num
+                            << " menu entry root; ";
                }
-                        
-               file <<
-                   // Set new menu location.
-                   "g1 = " << (1 + target.index
-                               + target_button_num * button_mult) << "; "
-                   // Jump to the target menu.
-                   "jump menu " << 1 + target.index << "; ";
-           }
-           else
-           {
-               assert(target.type == title_pgc);
 
-               file <<
-                   // Record current menu location.
-                   "g2 = " << (1 + menu_index
-                               + (1 + button_index) * button_mult) << "; "
-                   // Set target chapter number.
-                   "g3 = " << target.sub_index << "; "
-                   // Jump to the target title.
-                   "jump title " << 1 + target.index << "; ";
+               file <<  "</button>\n";
            }
 
-           file <<  "</button>\n";
+           file <<
+               "      </pgc>\n";
        }
 
-       file <<
-           "      </pgc>\n";
-    }
+       file << "    </menus>\n";
 
-    file <<
-       "    </menus>\n"
-       "  </vmgm>\n";
-    // Generate a titleset for each title.  This appears to make
-    // jumping to titles a whole lot simpler (but limits us to 99
-    // titles).
-    for (unsigned title_index = 0;
-        title_index != titles_.size();
-        ++title_index)
-    {
-       file <<
-           "  <titleset>\n"
-           // Generate a dummy menu so that the "menu" button will
-           // work.  This returns to the source menu via the title
-           // menu.
-           "    <menus>\n"
-           "      <pgc entry='root'>\n"
-           "        <pre> g1 = g2; jump vmgm menu; </pre>\n"
-           "      </pgc>\n"
-           "    </menus>\n"
-           "    <titles>\n"
-           "      <pgc>\n"
-           "        <pre>\n";
-
-       // Count chapters in the title.
-       unsigned n_chapters = 0;
-       for (vob_list::const_iterator
-                it = titles_[title_index].begin(),
-                end = titles_[title_index].end();
-            it != end;
-            ++it)
+       if (have_real_title)
        {
-           // Chapter start times may be specified in the "chapters"
-           // attribute as a comma-separated list.  If this is not
-           // specified then the beginning of each file starts a new
-           // chapter.  Thus the number of chapters in each file is
-           // the number of commas in the chapter attribute, plus 1.
-           ++n_chapters;
-           std::size_t pos = 0;
-           while ((pos = it->chapters.find(',', pos)) != std::string::npos)
+           file <<
+               "    <titles>\n"
+               "      <video format='" << frame_params_.common_name << "'/>\n"
+               "      <pgc>\n";
+
+           file << "        <pre>\n";
+
+           // Count chapters in the title.
+           unsigned n_chapters = 0;
+           for (vob_list::const_iterator
+                    it = titles_[titleset_num - 1].begin(),
+                    end = titles_[titleset_num - 1].end();
+                it != end;
+                ++it)
            {
+               // Chapter start times may be specified in the "chapters"
+               // attribute as a comma-separated list.  If this is not
+               // specified then the beginning of each file starts a new
+               // chapter.  Thus the number of chapters in each file is
+               // the number of commas in the chapter attribute, plus 1.
                ++n_chapters;
-               ++pos;
+               std::size_t pos = 0;
+               while ((pos = it->chapters.find(',', pos)) != std::string::npos)
+               {
+                   ++n_chapters;
+                   ++pos;
+               }
            }
-       }
 
-       // Generate jump "table" for chapters.
-       for (unsigned chapter_num = 1;
-            chapter_num <= n_chapters;
-            ++chapter_num)
-           file <<
-               "          if (g3 == " << chapter_num << ")\n"
-               "            jump chapter " << chapter_num << ";\n";
+           // Move the chapter number to scratch so the root menu can
+           // distinguish cases 4 and 5.
+           file << "          g0 = g3; g3 = 0;\n";
 
-       file <<
-           "        </pre>\n";
+           // Copy the latest menu location for use by the post-routine.
+           file << "          g4 = g2;\n";
 
-       for (vob_list::const_iterator
-                it = titles_[title_index].begin(),
-                end = titles_[title_index].end();
-            it != end;
-            ++it)
+           // Jump to the correct chapter.
+           for (unsigned chapter_num = 1;
+                chapter_num <= n_chapters;
+                ++chapter_num)
+               file <<
+                   "          if (g0 eq " << 1 + chapter_num << ")\n"
+                   "            jump chapter " << chapter_num << ";\n";
+
+           file << "        </pre>\n";
+
+           for (vob_list::const_iterator
+                    it = titles_[titleset_num - 1].begin(),
+                    end = titles_[titleset_num - 1].end();
+                it != end;
+                ++it)
+           {
+               file << "        <vob file='" << xml_escape(it->file) << "'";
+               if (!it->chapters.empty())
+                   file << " chapters='" << xml_escape(it->chapters) << "'";
+               if (!it->pause.empty())
+                   file << " pause='" << xml_escape(it->pause) << "'";
+               file << "/>\n";
+           }
+
+           // If the user has not exited to the menus and then
+           // resumed the title, set the latest menu location to be
+           // the button after the one that linked to this title.
+           // In any case, return to the (root) menu which will
+           // then jump to the correct menu.
+           file <<
+               "        <post>\n"
+               "          if (g2 eq g4)\n"
+               "            g2 = g2 + " << button_mult << ";\n"
+               "          call menu;\n"
+               "        </post>\n"
+               "      </pgc>\n"
+               "    </titles>\n";
+       }
+       else if (titleset_num != 0) // && !have_real_title
        {
-           file << "        <vob file='" << xml_escape(it->file) << "'";
-           if (!it->chapters.empty())
-               file << " chapters='" << xml_escape(it->chapters) << "'";
-           if (!it->pause.empty())
-               file << " pause='" << xml_escape(it->pause) << "'";
-           file << "/>\n";
+           file << "    <titles><pgc/></titles>\n";
        }
 
-       file <<
-           "        <post>\n"
-           // Return to the source menu, but highlight the next button.
-           "          g2 = g2 + " << button_mult << ";\n"
-           "          call menu;\n"
-           "        </post>\n"
-           "      </pgc>\n"
-           "    </titles>\n"
-           "  </titleset>\n";
+       file << "  </" << outer_element_name << ">\n";
     }
 
-    file <<
-       "</dvdauthor>\n";
-
+    file << "</dvdauthor>\n";
     file.close();
 
     {
@@ -566,7 +704,7 @@ void dvd_generator::generate(const std::string & output_dir) const
                             Glib::OWNERSHIP_NONE),
                         Glib::SPAWN_SEARCH_PATH
                         | Glib::SPAWN_STDOUT_TO_DEV_NULL,
-                        SigC::Slot0<void>(),
+                        sigc::slot<void>(),
                         0, 0,
                         &command_result);
        if (command_result != 0)