1 /*
2     dvdauthor -- generation of PGCs within IFO files
3 */
4 /*
5  * Copyright (C) 2002 Scott Smith (trckjunky@users.sourceforge.net)
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or (at
10  * your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20  * MA 02110-1301 USA.
21  */
22 
23 #include "compat.h"
24 #include <errno.h>
25 #include <assert.h>
26 
27 #include "dvdauthor.h"
28 #include "da-internal.h"
29 
30 
31 
32 #define MAXCELLS 4096
33 #define MAXPGCSIZE (236+128*8+256+MAXCELLS*(24+4))
34 #define BUFFERPAD (MAXPGCSIZE+1024)
35 
36 static int bigwritebuflen=0;
37 static unsigned char *bigwritebuf=0;
38 
39 
40 /*
41 titleset X: 2-255
42 vmgm: 1
43 menu Y: 1-119
44 menu entry Y: 120-127
45 title Y: 129-255
46 chapter Z: 1-255
47 
48 need 3 bytes plus jump command
49 
50 for VMGM menu:
51 [vmgm | titleset X] -> g[15] low
52 [menu [entry] Y | title Y] -> g[15] high
53 [chapter Z] -> g[14] low
54 
55 for VTSM root menu:
56 [menu [entry] Y | title Y] -> g[15] high
57 [chapter Z] -> g[15] low
58 
59 */
60 
genjumppad(unsigned char * buf,vtypes ismenu,int entry,const struct workset * ws,const struct pgcgroup * curgroup)61 static int genjumppad(unsigned char *buf, vtypes ismenu, int entry, const struct workset *ws, const struct pgcgroup *curgroup)
62   /* generates the jumppad if the user wants it. The code is put into buf, and the function
63     result is the number of bytes generated. */
64   {
65     unsigned char *cbuf = buf;
66     int i, j, k;
67     if (jumppad && ismenu == VTYPE_VTSM && entry == 7 /* root menu? */)
68       {
69         // *** VTSM jumppad
70         write8(cbuf,0x61,0x00,0x00,0x0E,0x00,0x0F,0x00,0x00); cbuf+=8; // g[14]=g[15];
71         write8(cbuf,0x71,0x00,0x00,0x0F,0x00,0x00,0x00,0x00); cbuf+=8; // g[15]=0;
72         // menu entry jumptable
73         for (i = 2; i < 8; i++)
74           {
75             for (j = 0; j < curgroup->numpgcs; j++)
76                 if (curgroup->pgcs[j]->entries & (1 << i))
77                   {
78                     write8(cbuf,0x20,0xA4,0x00,0x0E,i+120,0x00,0x00,j+1); cbuf+=8; // if g[14]==0xXX00 then LinkPGCN XX
79                   } /*if; for*/
80           } /*for*/
81         // menu jumptable
82         for (i = 0; i < curgroup->numpgcs; i++)
83           {
84             write8(cbuf,0x20,0xA4,0x00,0x0E,i+1,0x00,0x00,i+1); cbuf+=8; // if g[14]==0xXX00 then LinkPGCN XX
85           } /*for*/
86         // title/chapter jumptable
87         for (i = 0; i < ws->titles->numpgcs; i++)
88           {
89             write8(cbuf,0x71,0x00,0x00,0x0D,i+129,0,0x00,0x00); cbuf+=8; // g[13]=(i+1)*256;
90             write8(cbuf,0x30,0x23,0x00,0x00,0x00,i+1,0x0E,0x0D); cbuf+=8; // if g[15]==g[13] then JumpSS VTSM i+1, ROOT
91             for (j = 0; j < ws->titles->pgcs[i]->numchapters; j++)
92               {
93                 write8(cbuf,0x71,0x00,0x00,0x0D,i+129,j+1,0x00,0x00); cbuf+=8; // g[13]=(i+1)*256;
94                 write8(cbuf,0x30,0x25,0x00,j+1,0x00,i+1,0x0E,0x0D); cbuf+=8; // if g[15]==g[13] then JumpSS VTSM i+1, ROOT
95               } /*for*/
96           } /*for*/
97       }
98     else if (jumppad && ismenu == VTYPE_VMGM && entry == 2 /* title menu */)
99       {
100         // *** VMGM jumppad
101         // remap all VMGM TITLE X -> TITLESET X TITLE Y
102         k = 129;
103         for (i = 0; i < ws->titlesets->numvts; i++)
104             for (j = 0; j < ws->titlesets->vts[i].numtitles; j++)
105               {
106                 write8(cbuf, 0x71, 0xA0, 0x0F, 0x0F, j + 129, i + 2, k, 1);
107                   /* if g15 == k << 8 | 1 then g15 = j + 129 << 8 | i + 2 */
108                 cbuf += 8;
109                 k++;
110               } /*for; for*/
111         // move TITLE out of g[15] into g[14] (to mate up with CHAPTER)
112         // then put title/chapter into g[15], and leave titleset in g[14]
113         write8(cbuf,0x63,0x00,0x00,0x0E,0x00,0x0F,0x00,0x00); cbuf+=8; // g[14]+=g[15]
114         write8(cbuf,0x79,0x00,0x00,0x0F,0x00,0xFF,0x00,0x00); cbuf+=8; // g[15]&=255
115         write8(cbuf,0x64,0x00,0x00,0x0E,0x00,0x0F,0x00,0x00); cbuf+=8; // g[14]-=g[15]
116         write8(cbuf,0x62,0x00,0x00,0x0E,0x00,0x0F,0x00,0x00); cbuf+=8; // g[14]<->g[15]
117         // For each titleset, delegate to the appropriate submenu
118         for (i = 0; i < ws->titlesets->numvts; i++)
119           {
120             write8(cbuf,0x71,0x00,0x00,0x0D,0x00,i+2,0x00,0x00); cbuf+=8; // g[13]=(i+2)*256;
121             write8(cbuf,0x30,0x26,0x00,0x01,i+1,0x87,0x0E,0x0D); cbuf+=8; // if g[14]==g[13] then JumpSS VTSM i+1, ROOT
122           } /*for*/
123         // set g[15]=0 so we don't leak dirty registers to other PGC's
124         write8(cbuf,0x71,0x00,0x00,0x0F,0x00,0x00,0x00,0x00); cbuf+=8; // g[15]=0;
125       } /*if*/
126     return cbuf-buf;
127   } /*genjumppad*/
128 
jumppgc(unsigned char * buf,int pgc,vtypes ismenu,int entry,const struct workset * ws,const struct pgcgroup * curgroup)129 static int jumppgc(unsigned char *buf,int pgc,vtypes ismenu,int entry,const struct workset *ws,const struct pgcgroup *curgroup)
130   /* generates a command table for a PGC containing a jumppad. Returns the number of bytes output. */
131   {
132     int base = 0xEC; /* put command table immediately after PGC header */
133     int ncmd, offs;
134     offs = base + 8; /* room for command table header */
135     offs += genjumppad(buf + offs, ismenu, entry, ws, curgroup);
136     if (pgc > 0)
137         write8(buf + offs, 0x20, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, pgc); // LinkPGCN pgc
138     else
139         write8(buf + offs, 0x30, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); // JumpSS FP
140     offs += 8;
141     ncmd = (offs - base) / 8 - 1;
142     if (ncmd > 128)
143       {
144         fprintf
145           (
146             stderr,
147             "ERR:  Too many titlesets/titles/menus/etc for jumppad to handle."
148                 "  Reduce complexity and/or disable\njumppad.\n"
149           );
150         exit(1);
151       } /*if*/
152     buf[0xE5] = base; /* offset within PGC to command table, low byte */
153     buf[base + 1] = ncmd; /* number of pre commands, low byte */
154     write2(buf + base + 6, 7 + 8 * ncmd); /* end address relative to command table */
155     return offs;
156   } /*jumppgc*/
157 
genpgc(unsigned char * buf,const struct workset * ws,const struct pgcgroup * group,int pgc,vtypes ismenu,int entry)158 static int genpgc(unsigned char *buf,const struct workset *ws,const struct pgcgroup *group,int pgc,vtypes ismenu,int entry)
159 /* generates a PGC entry for an IFO file in buf. */
160   {
161     const struct vobgroup *va = (ismenu != VTYPE_VTS ? ws->menus->mg_vg : ws->titles->pg_vg);
162     const struct pgc * const thispgc = group->pgcs[pgc];
163     int i, j, d;
164 
165   /* buf[0], buf[1] don't seem to be used */
166 
167     // PGC header starts at buf[16]
168     buf[2] = thispgc->numprograms;
169     buf[3] = thispgc->numcells;
170     write4(buf + 4, buildtimeeven(va, getptsspan(thispgc))); /* BCD playback time + frame rate */
171   /* buf[8 .. 11] -- prohibited user ops -- none for now */
172     for (i = 0; i < va->numaudiotracks; i++)
173       {
174         if (va->ad[i].aid)
175             buf[12 + i * 2] = 0x80 | (va->ad[i].aid - 1);
176           /* PGC_AST_CTL, audio stream control: actual stream IDs corresponding to
177             each stream described in IFO */
178       } /*for*/
179     for (i = 0; i < va->numsubpicturetracks; i++)
180       {
181         int m;
182         bool e;
183         e = false;
184         for (m = 0; m < 4; m++)
185             if (thispgc->subpmap[i][m] & 128)
186               {
187                 buf[28 + i * 4 + m] = thispgc->subpmap[i][m] & 127;
188                   /* PGC_SPST_CTL, subpicture stream control: actual stream IDs corresponding
189                     to each stream described in IFO */
190                 e = true;
191               } /*if; for*/
192         if (e)
193             buf[28 + i * 4] |= 0x80; /* set stream-available flag */
194       } /*for*/
195     buf[163] = thispgc->pauselen; // PGC stilltime
196     for (i = 0; i < 16; i++) /* colour lookup table (0, Y, Cr, Cb) */
197         write4
198           (
199             buf + 164 + i * 4,
200             thispgc->colors->color[i] < COLOR_UNUSED ? thispgc->colors->color[i] : 0x108080
201           );
202 
203     d = 0xEC; /* start assembling commands here */
204     // command table
205       {
206         unsigned char *cd = buf + d + 8, *preptr, *postptr, *cellptr;
207         preptr = cd; /* start of pre commands */
208         cd += genjumppad(cd, ismenu, entry, ws, group);
209         if (thispgc->prei)
210           {
211             cd = vm_compile(preptr, cd, ws, thispgc->pgcgroup, thispgc, thispgc->prei, ismenu);
212             if (!cd)
213               {
214                 fprintf(stderr, "ERR:  in %s pgc %d, <pre>\n", pstypes[ismenu], pgc);
215                 exit(1);
216               } /*if*/
217           }
218         else if (thispgc->numbuttons)
219           {
220             write8(cd, 0x56, 0x00, 0x00, 0x00, 4 * 1, 0x00, 0x00, 0x00); cd += 8;
221               // set active button to be #1
222           } /*if*/
223 
224         postptr = cd; /* start of post commands */
225         if (thispgc->numbuttons && !allowallreg)
226           {
227           /* transfer sequence for button instructions too long to fit in PCI packet */
228           /* enter with g15 = button number */
229             write8(cd, 0x61, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x00);  // g[14]=g[15]
230             write8(cd + 8, 0x71, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00); // g[15]=0
231             cd += 8 * (thispgc->numbuttons + 2);
232               /* transfer to individual button sequences--reserve space to be filled in below */
233           } /*if*/
234         if (thispgc->posti)
235           {
236             cd = vm_compile(postptr, cd, ws, thispgc->pgcgroup, thispgc, thispgc->posti, ismenu);
237             if (!cd)
238               {
239                 fprintf(stderr, "ERR:  in %s pgc %d, <post>\n", pstypes[ismenu], pgc);
240                 exit(1);
241               } /*if*/
242           }
243         else
244           {
245             write8(cd, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); // exit
246             cd += 8;
247           } /*if*/
248         if (thispgc->numbuttons && !allowallreg)
249           {
250           /* compile and insert all the button instructions that wouldn't fit
251             in the PCI packets in the VOB */
252             unsigned char *buttonptr = cd;
253             for (i = 0; i < thispgc->numbuttons; i++)
254               {
255                 const struct button * const thisbutton = &thispgc->buttons[i];
256                 unsigned char * cdd = vm_compile(postptr, cd, ws, thispgc->pgcgroup, thispgc, thisbutton->commands, ismenu);
257                 if (!cdd)
258                   {
259                     fprintf(stderr, "ERR:  in %s pgc %d, button %s\n", pstypes[ismenu], pgc, thisbutton->name);
260                     exit(1);
261                   } /*if*/
262                 if (cdd == cd + 8)
263                   {
264                     // the button fits in one command; assume it went in the vob itself
265                     memset(postptr + i * 8 + 16, 0, 8); // nop
266                     memset(cd, 0, 8);
267                       // reset the just compiled command, since it was test written
268                       // to part of the pgc structure
269                   }
270                 else
271                   {
272                     write8(postptr + i * 8 + 16, 0x00, 0xa1, 0x00, 0x0E, 0x00, i + 1, 0x00, (cd - postptr) / 8 + 1);
273                       /* if g14 == i + 1 then goto (cd - postptr) / 8 + 1 */
274                       /* insert entry in transfer table */
275                     cd = cdd;
276                   } /*if*/
277               } /*for*/
278             if (cd == buttonptr)
279                 memset(postptr, 0, 16);
280                   /* no button transfer sequences generated, nop the register transfer */
281           } /*if*/
282 
283         vm_optimize(postptr, postptr, &cd);
284           /* tidy up all the code I just output */
285 
286         cellptr = cd;
287         for (i = 0; i < thispgc->numsources; i++)
288             for (j = 0; j < thispgc->sources[i]->numcells; j++)
289               {
290                 const struct cell * const thiscell = &thispgc->sources[i]->cells[j];
291                 if (thiscell->commands)
292                   {
293                     unsigned char *cdd = vm_compile(cellptr, cd, ws, thispgc->pgcgroup, thispgc, thiscell->commands, ismenu);
294                     if (!cdd)
295                       {
296                         fprintf(stderr, "ERR:  in %s pgc %d, <cell>\n", pstypes[ismenu], pgc);
297                         exit(1);
298                       } /*if*/
299                     if (cdd != cd + 8)
300                       {
301                         fprintf(stderr, "ERR:  Cell command can only compile to one VM instruction.\n");
302                         exit(1);
303                       } /*if*/
304                     cd = cdd;
305                   } /*if*/
306               } /*for; for*/
307 
308         write2(buf + 228, d); /* offset to commands */
309         if (cd - (buf + d) - 8 > 128 * 8) // can only have 128 commands
310           {
311             fprintf(stderr, "ERR:  Can only have 128 commands for pre, post, and cell commands.\n");
312             exit(1);
313           } /*if*/
314       /* fill in command table header */
315         write2(buf + d, (postptr - preptr) / 8); /* nr pre commands */
316         write2(buf + d + 2, (cellptr - postptr) / 8); /* nr post commands */
317         write2(buf + d + 4, (cd - cellptr) / 8); /* nr cell commands */
318         write2(buf + d + 6, cd - (buf + d) - 1); /* end address relative to command table */
319         d = cd - buf;
320       }
321 
322     if (thispgc->numsources)
323       {
324       /* fill in program map, cell playback info table and cell position info table */
325         int cellline;
326         bool notseamless;
327         write2(buf + 230, d); /* offset to program map */
328         j = 1;
329         for (i = 0; i < thispgc->numsources; i++)
330           { /* generate the program map */
331             int k;
332             const struct source * const thissource = thispgc->sources[i];
333             for (k = 0; k < thissource->numcells; k++)
334               {
335                 if (thissource->cells[k].scellid == thissource->cells[k].ecellid)
336                     continue; /* empty cell */
337                 if (thissource->cells[k].ischapter != CELL_NEITHER)
338                     buf[d++] = j; /* this cell starts a program (possibly a chapter as well) */
339                 j += thissource->cells[k].ecellid - thissource->cells[k].scellid;
340               } /*for*/
341           } /*for*/
342         d += d & 1; /* round up to word boundary */
343 
344         write2(buf + 232, d); /* offset to cell playback information table */
345         j = -1;
346         cellline = 1;
347         notseamless = true; /* first cell never seamless, otherwise a/v sync problems will occur */
348         for (i = 0; i < thispgc->numsources; i++)
349           { /* generate the cell playback information table */
350             int k, l, m, firsttime;
351             const struct source * const thissource = thispgc->sources[i];
352             for (k = 0; k < thissource->numcells; k++)
353                 for (l = thissource->cells[k].scellid; l < thissource->cells[k].ecellid; l++)
354                   {
355                     const int id = thissource->vob->vobid * 256 + l;
356                     int vi;
357                     for (m = 0; m < va->numaudiotracks; m++)
358                         if
359                           (
360                                 j >= 0
361                             &&
362                                 getaudch(va, m) >= 0
363                             &&
364                                 calcaudiogap(va, j, id, getaudch(va, m))
365                           )
366                             notseamless = true;
367                     buf[d] =
368                             (notseamless ? 0 : 8 /* seamless multiplex */)
369                         |
370                             (j + 1 != id ? 2 /* SCR discontinuity */ : 0);
371                               // if this is the first cell of the source, then set 'STC_discontinuity', otherwise set 'seamless presentation'
372                     notseamless = false; /* initial assumption for next cell */
373                     j = id;
374 
375                     vi = findcellvobu(thissource->vob, l);
376                     firsttime = thissource->vob->vobu[vi].sectpts[0];
377                     write4(buf + 8 + d, thissource->vob->vobu[vi].sector);
378                       /* first VOBU start sector */
379                   /* first ILVU end sector not supported for now */
380                     vi = findcellvobu(thissource->vob, l + 1) - 1;
381                     if (l == thissource->cells[k].ecellid - 1)
382                       {
383                         if (thissource->cells[k].pauselen > 0)
384                           {
385                             buf[2 + d] = thissource->cells[k].pauselen; /* cell still time */
386                             notseamless = true; // cells with stilltime are not seamless
387                           } /*if*/
388                         if (thissource->cells[k].commands)
389                           {
390                             buf[3 + d] = cellline++; /* cell command nr */
391                             notseamless = true; // cells with commands are not seamless
392                           } /*if*/
393                       } /*if*/
394                     write4(buf + 4 + d, buildtimeeven(va, thissource->vob->vobu[vi]. sectpts[1] - firsttime));
395                       /* cell playback time + frame rate */
396                     write4(buf + 16 + d, thissource->vob->vobu[vi].sector);
397                       /* last VOBU start sector */
398                     write4(buf + 20 + d, thissource->vob->vobu[vi].lastsector);
399                       /* last VOBU end sector */
400                     d += 24;
401                   } /*for; for*/
402           } /*for*/
403 
404         write2(buf + 234, d); /* offset to cell position information table */
405         for (i = 0; i < thispgc->numsources; i++)
406           { /* build the cell position information table */
407             int j,k;
408             const struct source * const thissource = thispgc->sources[i];
409             for (j = 0; j < thissource->numcells; j++)
410                 for (k = thissource->cells[j].scellid; k < thissource->cells[j].ecellid; k++)
411                   { /* put in IDs of all nonempty cells */
412                     buf[1 + d] = thissource->vob->vobid; /* VOB ID low byte */
413                     buf[3 + d] = k; /* cell ID */
414                     d += 4;
415                   } /*for*/
416           } /*for*/
417       } /*if thispgc->numsources*/
418 
419     return d;
420   } /*genpgc*/
421 
createpgcgroup(const struct workset * ws,vtypes ismenu,const struct pgcgroup * va,unsigned char * buf)422 static int createpgcgroup(const struct workset *ws,vtypes ismenu,const struct pgcgroup *va,unsigned char *buf /* where to put generated table */)
423   /* generates a VMGM_LU, VTSM_LU or VTS_PGCI table and all associated PGCs. Returns -1 if there wasn't enough space. */
424   {
425     int len, i, pgcidx, nrtitles;
426     len = va->numpgcs + va->numentries;
427       /* duplicate each PGC for each additional menu entry type it may have ... */
428     for (i = 0; i < va->numpgcs; i++)
429         if (va->pgcs[i]->entries)
430             len--; /* ...  beyond the first one */
431     write2(buf, len); /* nr language units (VMGM_LU, VTSM_LU)/program chains (VTS_PGCI) */
432     len = len * 8 + 8; /* total length of VMGM_LU/VTSM_LU/VTS_PGCI */
433     pgcidx = 8;
434     nrtitles = 0;
435     for (i = 0; i < va->numpgcs; i++)
436       { /* generate the PGCs and put in their offsets */
437         int j = 0;
438         if (buf + len + BUFFERPAD - bigwritebuf > bigwritebuflen)
439             return -1; /* caller needs to give me more space */
440         if (ismenu == VTYPE_VTS)
441           {
442             if (va->pgcs[i]->entries == 0)
443               {
444                 ++nrtitles;
445                 buf[pgcidx] = 0x80 | nrtitles; /* title type & nr for VTS_PGC */
446               } /*if*/
447           }
448         else
449           {
450             buf[pgcidx] = 0;
451             for (j = 2; j < 8; j++)
452                 if (va->pgcs[i]->entries & (1 << j))
453                   {
454                     buf[pgcidx] = 0x80 | j; /* menu type for VMGM_LU/VTSM_LU */
455                     break;
456                   } /*if; for*/
457           } /*if*/
458         write4(buf + pgcidx + 4, len); /* offset to VMGM_PGC/VTSM_PGC/VTS_PGC */
459         len += genpgc(buf + len, ws, va, i, ismenu, j); /* put it there */
460         pgcidx += 8;
461       } /*for*/
462     for (i = 2; i < 8; i++)
463       {
464         if (va->allentries & (1 << i))
465           {
466             int j;
467             if (buf + len + BUFFERPAD - bigwritebuf > bigwritebuflen)
468                 return -1; /* caller needs to give me more space */
469             for (j = 0; j < va->numpgcs; j++)
470                 if (va->pgcs[j]->entries & (1 << i))
471                     break;
472             if (j < va->numpgcs) // this can be false if jumppad is set
473               {
474                 // is this the first entry for this pgc? if so, it was already
475                 // triggered via the PGC itself, so skip writing the extra
476                 // entry here
477                 if ((va->pgcs[j]->entries & ((1 << i) - 1)) == 0)
478                     continue;
479               } /*if*/
480             j++;
481             buf[pgcidx] = 0x80 | i; /* menu type */
482             write4(buf + pgcidx + 4, len); /* offset to VMGM_PGC/VTSM_PGC */
483             len += jumppgc(buf + len, j, ismenu, i, ws, va);
484             pgcidx += 8;
485           } /*if*/
486       } /*for*/
487     write4(buf + 4, len - 1);
488       /* end address (last byte of last PGC in this LU) relative to VMGM_LU/VTSM_LU */
489     return len;
490   } /*createpgcgroup*/
491 
CreatePGC(FILE * h,const struct workset * ws,vtypes ismenu)492 int CreatePGC(FILE *h, const struct workset *ws, vtypes ismenu)
493   {
494     unsigned char *buf;
495     int len,ph,i;
496     bool in_it;
497 
498     in_it = bigwritebuflen == 0; /* don't do first allocation if already got a buffer from last time */
499  retry: /* come back here if buffer wasn't big enough to generate a PGC group */
500     if (in_it)
501       {
502         if (bigwritebuflen == 0)
503             bigwritebuflen = 128 * 1024; /* first call */
504         else
505             bigwritebuflen <<= 1; /* previous write failed, try twice the size */
506         if (bigwritebuf)
507             free(bigwritebuf);
508         bigwritebuf = malloc(bigwritebuflen);
509       } /*if*/
510     in_it = true;
511     buf = bigwritebuf;
512     memset(buf, 0, bigwritebuflen);
513     if (ismenu != VTYPE_VTS) /* create VMGM_PGCI_UT/VTSM_PGCI_UT structure */
514       {
515         buf[1] = ws->menus->numgroups; // # of language units
516         ph = 8 + 8 * ws->menus->numgroups; /* VMGM_LU/VTSM_LU structures start here */
517 
518         for (i = 0; i < ws->menus->numgroups; i++)
519           {
520             unsigned char * const plu = buf + 8 + 8 * i;
521             const struct langgroup * const lg = &ws->menus->groups[i];
522             memcpy(plu, lg->lang, 2); /* ISO639 language code */
523             if (ismenu == VTYPE_VTSM)
524                 plu[3] = lg->pg->allentries;
525             else /* ismenu = VTYPE_VMGM */
526                 plu[3] = 0x80; // menu system contains entry for title
527             write4(plu + 4, ph);
528             len = createpgcgroup(ws, ismenu, lg->pg, buf + ph);
529             if (len < 0)
530                 goto retry;
531             ph += len;
532           } /*for*/
533         write4(buf + 4, ph - 1);
534           /* end address (last byte of last PGC in last LU) relative to VMGM_PGCI_UT/VTSM_PGCI_UT */
535       }
536     else
537       {
538       /* generate VTS_PGCI structure */
539         len = createpgcgroup(ws, VTYPE_VTS, ws->titles, buf);
540         if (len < 0)
541             goto retry;
542         ph = len;
543       } /*if*/
544 
545     assert(ph <= bigwritebuflen);
546     ph = (ph + 2047) & (-2048);
547     if (h)
548       {
549         (void)fwrite(buf, 1, ph, h);
550         if (errno != 0)
551           {
552             fprintf
553               (
554                 stderr,
555                 "\nERR:  Error %d -- %s -- writing output PGC\n",
556                 errno,
557                 strerror(errno)
558               );
559             exit(1);
560           } /*if*/
561       } /*if*/
562     return ph / 2048;
563   } /*CreatePGC*/
564