]> git.decadent.org.uk Git - videolink.git/commitdiff
Rewrote navigation code generator to support menus spread across multiple domains.
authorBen Hutchings <ben@decadent.org.uk>
Thu, 30 Nov 2006 02:51:14 +0000 (02:51 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 2 Nov 2008 23:58:12 +0000 (23:58 +0000)
This raises the maximum number of menus to 999 (would be 1023 but we only use 3 digits in temporary file serial numbers).

README
TODO
generate_dvd.cpp

diff --git a/README b/README
index e51a3e6c240ce2ccd36ecc2e8f12f289042fb9ba..fa2fdddf9828f85bb23237c50be013ded30c1175 100644 (file)
--- a/README
+++ b/README
@@ -176,11 +176,9 @@ aspect ratio and sample rate.  However, each chapter will run into the
 next.  If this is a real problem, let me know, and I may be able to
 provide a better solution in a later version of VideoLink.
 
-There is a limit of 128 menus in each "domain" of a DVD, which is
-further reduced by dvdauthor to a practical limit of 119 menus.
-Currently VideoLink generates all menus in a single domain (the VMGM
-domain).  Later versions of VideoLink should support larger numbers of
-menus by using multiple domains.
+Currently VideoLink code can generate up to 999 menus but it may be
+possible for me to raise this internal limit if you want more.  There
+appears to be an absolute limit of 12800 menus on a DVD.
 
 Bugs
 ----
diff --git a/TODO b/TODO
index 18c4fb94fc3afd8c3a56c343704f80739d2147a6..468f2929144c9d71ba54ffc93687d7d25cc71a04 100644 (file)
--- a/TODO
+++ b/TODO
@@ -5,7 +5,6 @@ Use ffmpeg to convert unsuitable video files (how do we check this?).
 Add keyboard command for "top menu" in preview mode.
 Add support for videos in preview mode.
 Support more than 99 titles somehow (grouping them into titlesets won't help)
-Support more than 119 menus somehow.
 Track down and fix/suppress the NS_BINDING_ABORTED (0x804b002) error that occasionally appears in preview mode.
 
 Priority 3 (lowest)
index 5d357e6d39c385fe6eb76a89551bd5c115340e64..2fa3388d6bb1e5c6b6e1e8d3c1835ba4d095b0cc 100644 (file)
@@ -105,6 +105,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 +129,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;
@@ -299,257 +310,387 @@ 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)
-       {
-           // 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
+       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";
+
+       const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
+       const unsigned menu_end =
+           have_real_menus
+           ? std::min((titleset_num + 1) * dvdauthor_anonymous_menus_max,
+                      menus_.size())
+           : menu_begin + 1;
+
+       for (unsigned menu_index = menu_begin;
+            menu_index != menu_end;
+            ++menu_index)
        {
-           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"
+               "      <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";
+
+           // 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";
 
-       for (vob_list::const_iterator
-                it = titles_[title_index].begin(),
-                end = titles_[title_index].end();
-            it != end;
-            ++it)
+           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();
 
     {