PR #23851 opened by superuser404
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23851
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23851.patch

`parse_presentation_segment()` flushes the palette and object caches for every 
`composition_state != 0`. Composition state 3 (epoch continue) however 
continues the previous epoch: when decoding linearly, the retained palettes and 
objects stay valid, and streams carry bare PCS + WDS + END display sets at 
connection points that reference them without re-supplying the data.

Flushing on epoch continue drops exactly the data such a display set 
references. Decoding then fails with `Invalid palette id` / `Invalid object id` 
and the subtitle is missing on screen. I have seen this in the wild on 
BD-sourced PGS streams whose authoring splits at connection points.

With this patch only acquisition point (1) and epoch start (2) release the 
cache.

### Reproducer

The generator below writes a minimal .sup: display set 1 (epoch start) defines 
a palette and an 8x8 object and presents it, display set 2 (epoch continue) is 
a bare PCS + WDS + END referencing the retained data.

Before:

```
$ ffprobe -select_streams s:0 -show_frames -show_entries frame=pts_time 
epoch_continue.sup
pts_time=1.000000
[pgssub @ ...] Invalid palette id 0
```

After:

```
pts_time=1.000000
pts_time=3.000000
```

<details>
<summary>gen_pgs_epoch_continue.py</summary>

```python
#!/usr/bin/env python3
import struct
import sys

def seg(pts_90k, seg_type, payload):
    return b"PG" + struct.pack(">IIBH", pts_90k, 0, seg_type, len(payload)) + 
payload

def pcs(comp_num, state, num_objs):
    p = struct.pack(">HHBHBBBB", 720, 480, 0x10, comp_num, state, 0x00, 0, 
num_objs)
    if num_objs:
        p += struct.pack(">HBBHH", 0, 0, 0x00, 100, 100)  # obj 0 in window 0 
at (100,100)
    return p

def wds():
    return struct.pack(">BBHHHH", 1, 0, 100, 100, 8, 8)

def pds():
    entries = struct.pack(">BBBBB", 0, 0x00, 0x80, 0x80, 0x00)   # entry 0: 
transparent
    entries += struct.pack(">BBBBB", 1, 0xEB, 0x80, 0x80, 0xFF)  # entry 1: 
opaque white
    return struct.pack(">BB", 0, 0) + entries

def ods():
    rle = (b"\x01" * 8 + b"\x00\x00") * 8  # 8x8 solid color 1, one run per line
    data_len = 4 + len(rle)                # width + height fields + RLE payload
    return struct.pack(">HBB", 0, 0, 0xC0) + data_len.to_bytes(3, "big") + \
           struct.pack(">HH", 8, 8) + rle

out = b""
t1 = 90000   # display set 1: epoch start at 1s
out += seg(t1, 0x16, pcs(0, 0x80, 1))
out += seg(t1, 0x17, wds())
out += seg(t1, 0x14, pds())
out += seg(t1, 0x15, ods())
out += seg(t1, 0x80, b"")
t2 = 270000  # display set 2: epoch continue at 3s, bare PCS+WDS+END
out += seg(t2, 0x16, pcs(1, 0xC0, 1))
out += seg(t2, 0x17, wds())
out += seg(t2, 0x80, b"")

with open(sys.argv[1] if len(sys.argv) > 1 else "epoch_continue.sup", "wb") as 
f:
    f.write(out)
```

</details>

Note: this touches the same hunk as the pending decoder rework in #21109, which 
keeps the flush-on-any-nonzero-state behavior. The two changes are independent; 
I am happy to rebase this on top of #21109 if that lands first.



>From 1fa66ef502288d300ab44ab16287b34fd9e86280 Mon Sep 17 00:00:00 2001
From: Vincent Herbst <[email protected]>
Date: Sun, 19 Jul 2026 20:10:22 +0200
Subject: [PATCH] avcodec/pgssubdec: keep cached palettes and objects on epoch
 continue

parse_presentation_segment() flushes the palette and object caches for
every composition_state != 0. composition_state 3 (epoch continue)
however continues the previous epoch: when decoding linearly, the
retained palettes and objects stay valid, and streams carry bare
PCS + WDS + END display sets at connection points that reference them
without re-supplying the data.

Flushing on epoch continue drops exactly the data such a display set
references, decoding fails with "Invalid palette id" / "Invalid object
id", and the subtitle is missing on screen. Seen in the wild on
BD-sourced PGS streams whose authoring splits at connection points.

Only acquisition point (1) and epoch start (2) release the cache now.

A minimal reproducer is an epoch start display set defining one object
followed by an epoch continue display set with a bare PCS + WDS + END
referencing it.
---
 libavcodec/pgssubdec.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/libavcodec/pgssubdec.c b/libavcodec/pgssubdec.c
index 20583c9afa..079808ac4a 100644
--- a/libavcodec/pgssubdec.c
+++ b/libavcodec/pgssubdec.c
@@ -416,12 +416,15 @@ static int parse_presentation_segment(AVCodecContext 
*avctx,
      * 00 - Normal, previously defined objects and palettes are still valid
      * 01 - Acquisition point, previous objects and palettes can be released
      * 10 - Epoch start, previous objects and palettes can be released
-     * 11 - Epoch continue, previous objects and palettes can be released
+     * 11 - Epoch continue, the epoch continues across a connection point;
+     *      when decoding linearly the previously defined objects and
+     *      palettes remain valid and display sets may reference them
+     *      without re-supplying the data
      *
      * reserved 6 bits discarded
      */
     state = bytestream_get_byte(&buf) >> 6;
-    if (state != 0) {
+    if (state != 0 && state != 3) {
         flush_cache(avctx);
     }
 
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to