// 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,
{
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;
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 & " << 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 & " << 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 & " << 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 & " << 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();
{