X-Git-Url: https://git.decadent.org.uk/gitweb/?p=videolink.git;a=blobdiff_plain;f=generate_dvd.cpp;h=e28659e1be467c225738b9044aa7c21b326eee8a;hp=5d357e6d39c385fe6eb76a89551bd5c115340e64;hb=e1f43b883bb767bc7f50b7c96b1579c0dd5b9290;hpb=3a7baf39d3211c0bda3de1ab2b657e024a35b8af diff --git a/generate_dvd.cpp b/generate_dvd.cpp index 5d357e6..e28659e 100644 --- a/generate_dvd.cpp +++ b/generate_dvd.cpp @@ -1,4 +1,4 @@ -// Copyright 2005-6 Ben Hutchings . +// Copyright 2005-8 Ben Hutchings . // See the file "COPYING" for licence details. #include @@ -10,6 +10,10 @@ #include #include +#include +#include +#include + #include #include #include @@ -105,6 +109,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 +133,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 +165,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 +215,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 << " " << 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( argv, sizeof(argv)/sizeof(argv[0]), Glib::OWNERSHIP_NONE), Glib::SPAWN_STDOUT_TO_DEV_NULL, - SigC::Slot0(), + sigc::slot(), 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 +303,388 @@ 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 << "\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 << - "\n" - " \n" - " \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( + 1U + std::max(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" + << " \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) { - // This is the first (title) menu, displayed when the - // disc is first played. - file << - " \n" - "
\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 <<
-		"      \n"
-		"        
\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 <<
+		    "      \n"
+		    "        
\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 <<
+		    "      \n"
+		    "        
\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 <<
+		    "      \n"
+		    "        
\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 
 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";
+		    "        
\n" + " \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" - "
\n" - " \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. - " \n" - " \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 << "
\n" + " \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. + " \n" + " \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 << " \n"; } - file << "\n"; + file << + "
\n"; } - file << - " \n"; - } + file << " \n"; - file << - " \n" - " \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 << - " \n" - // Generate a dummy menu so that the "menu" button will - // work. This returns to the source menu via the title - // menu. - " \n" - " \n" - "
 g1 = g2; jump vmgm menu; 
\n" - "
\n" - "
\n" - " \n" - " \n" - "
\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 <<
+		"    \n"
+		"      \n";
+
+	    file << "        
\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 <<
-	    "        
\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 << "
\n"; + + for (vob_list::const_iterator + it = titles_[titleset_num - 1].begin(), + end = titles_[titleset_num - 1].end(); + it != end; + ++it) + { + file << " 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 << + " \n" + " if (g2 eq g4)\n" + " g2 = g2 + " << button_mult << ";\n" + " call menu;\n" + " \n" + "
\n" + "
\n"; + } + else if (titleset_num != 0) // && !have_real_title { - file << " chapters.empty()) - file << " chapters='" << xml_escape(it->chapters) << "'"; - if (!it->pause.empty()) - file << " pause='" << xml_escape(it->pause) << "'"; - file << "/>\n"; + file << " \n"; } - file << - " \n" - // Return to the source menu, but highlight the next button. - " g2 = g2 + " << button_mult << ";\n" - " call menu;\n" - " \n" - " \n" - " \n" - "
\n"; + file << " \n"; } - file << - "\n"; - + file << "\n"; file.close(); { @@ -566,7 +701,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(), + sigc::slot(), 0, 0, &command_result); if (command_result != 0)