Skip to content

feat(mpeg): host-fed CD-stream ES injection for the guest-driven decoder - #178

Open
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/18-mpeg-host-feed
Open

feat(mpeg): host-fed CD-stream ES injection for the guest-driven decoder#178
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/18-mpeg-host-feed

Conversation

@smmathews

@smmathews smmathews commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Host-fed CD-stream ES injection for the guest-driven MPEG decoder

Problem

Upstream's guest-driven MPEG decoder ingests elementary stream only through
the guest's own demux syscalls, which read from guest RAM at a guest
pointer. Runtimes that HLE the movie stream's IOP-side delivery — an IOP
streamer that fills an EE ring the runtime does not model — hold a correct,
in-order ES in host memory with the guest's sceMpegGetPicture consumer
live, but have no supported seam to hand those bytes to the decoder. They
are forced to reimplement the guest ring protocol or carry a parallel
downstream decoder. The same gap also applies when a runtime applies an
on-disc stream transform host-side before decode.

Fix

Add size_t feedMpegCdStreamBytes(const uint8_t *, size_t): the missing
data verb to go with the existing control verbs
notifyMpegCdStreamStart / notifyMpegCdStreamEof. It routes host bytes
into the playback state(s) on the current CD-stream generation and demuxes
them through the exact same PSS path as sceMpegDemuxPss, fanning out to
every open decoder on that generation. No mpegAddr argument — a host
streamer does not know the guest handle; routing mirrors the existing
generation bookkeeping. Returns size while a CD stream is active — whether
the bytes are routed to the open decoder(s) or staged for a decoder not yet
created on this generation — and 0 only when no CD stream is active.

Feed ordering / staging

Host feed is order-insensitive with respect to sceMpegCreate. Bytes fed
after notifyMpegCdStreamStart but before a decoder exists on the current
CD-stream generation are staged in host memory (bounded by a fixed cap; on
overflow the stage is discarded and a later decoder resyncs on live feed)
and replayed into the next decoder created on that generation before its
first getPicture. Staged bytes are discarded on stream stop or restart.
This closes a silent-drop window where bytes fed before the first
sceMpegCreate were dropped and the later decoder could not recover the
prefix. A single decoder is the supported shape; a second decoder that opens
mid-generation after bytes have already routed to the first resyncs at the
next sequence header, as with guest demux.

Scope / why generic

No game knowledge: no file layout, no transform, no addresses. Symmetric
with and reusing the merged notifyMpegCdStream* contract. Inert for every
existing title (no call site invokes it; it early-outs when no CD stream is
active). Internally a pure addition: one header declaration, one new public
function, and a single defaulted parameter on the internal appendPssBytes
that leaves all existing callers and the guest demux/callback path
unchanged.

No in-tree consumer

Nothing in ps2xRuntime calls this yet. It is an embedder seam for the
runtime library — the missing data verb alongside the existing
notifyMpegCdStream* control verbs — so downstream embedders that
produce the elementary stream host-side can feed the upstream decoder
instead of carrying a parallel one.

Maintainer-attention point (callback design)

A host feed has no calling guest thread, so guest stream callbacks cannot be
dispatched from it. This PR ships the drop-callbacks variant (documented
with // TODO(host-feed callbacks)): host-fed data records no guest
addresses, so no callback event is ever queued. If a future title needs
stream callbacks on a host-fed stream, the follow-up is a small
per-MpegPlaybackState event queue drained by the next guest MPEG syscall.
Flagged for your call.

Testing

Full suite passes — run:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1 && cmake --build build && ./build/ps2xTest/ps2x_tests

The original routing-contract tests are kept as-is: a start/feed/getPicture/
eof round-trip that decodes a host-fed program stream with no
sceMpegDemuxPss* call in the path, a feed-before-start no-op, and a
two-decoder fan-out test that pins the input range being reported once (not
once per decoder) and that every open decoder on the current generation
receives the fed bytes. New tests cover the pre-decoder staging path added
in this revision: a feed staged before any decoder exists reaching the
decoder created afterward (and reporting the feed as accepted, not dropped),
a packet split across two feed calls, decoding across a CD-stream restart, a
staging-cap overflow with a per-generation recovery leg, and a staged-but-
never-consumed feed being discarded at stream stop rather than resurrected by
a decoder created after EOF. The new tests require an ffmpeg-enabled build,
the same as every existing MPEG decode test.

Each pinned property has an isolating source-level mutation. Two properties
share pre-existing partial-buffer machinery and fail as a pair rather than
singly, and one defensive restart guard is provably redundant (no public-API
mutation isolates it); these non-isolating cases are expected and were
verified deliberately, not glossed. Full mutation matrix available on
request.

Note on the return value: it is size (the whole input range) while a CD
stream is active, whether the bytes are routed live or staged, and 0 only
when no CD stream is active — it is not a decoded-byte count and does not
scale with how many decoders are open.

@smmathews
smmathews marked this pull request as ready for review July 21, 2026 18:49
@ran-j

ran-j commented Jul 22, 2026

Copy link
Copy Markdown
Owner

I think I found a but on this PR but still checking

Add feedMpegCdStreamBytes(), the missing data verb alongside the existing
notifyMpegCdStreamStart/Eof control verbs. It lets an embedder that services
the movie stream host-side (an HLE'd IOP streamer, or an on-disc stream
transform applied before decode) push elementary-stream bytes straight into
the guest-driven decoder instead of reimplementing the guest ring protocol
or carrying a second, parallel decoder. Bytes route to every open decoder on
the current CD-stream generation and demux through the same PSS path as
sceMpegDemuxPss. There is no guest thread behind a host feed, so stream
callbacks are not dispatched on this path yet; that gap is flagged with a
TODO for a follow-up per-decoder event queue rather than silently dropped.

Feed is order-insensitive with respect to sceMpegCreate. Previously, bytes
fed before any decoder existed on a generation were silently lost: the
routing loop found nothing to fan out to and returned 0, indistinguishable
from "no active stream," and a decoder created afterward had no way to
recover the missing prefix. Bytes fed before a decoder exists are now held
in a bounded per-generation stage and replayed into the next decoder created
on that generation before its first getPicture. The stage is discarded on
overflow, stream stop, and stream restart, and the return value is `size`
whenever a CD stream is active whether the bytes were routed live or staged,
so a caller can no longer read "accepted but staged" as "rejected."

Pure addition otherwise: a header constant plus one function declaration, one
new public function, a defaulted parameter on the internal appendPssBytes, and
nothing else in ps2xRuntime calls it — no existing call site or behavior
changes for any current title.
@smmathews
smmathews force-pushed the feature/18-mpeg-host-feed branch from 377393a to ef783c6 Compare July 26, 2026 14:21
@smmathews

Copy link
Copy Markdown
Contributor Author

While auditing our side I did find one concrete bug, in case it's the same thing you hit: feedMpegCdStreamBytes only routed to decoders that already existed on the current CD-stream generation, so bytes fed after notifyMpegCdStreamStart but before the guest's first sceMpegCreate were silently dropped (return 0, no staging), and a decoder opened later never recovered that prefix. The return value also couldn't distinguish "no active stream" from "active but no decoder yet."

I've fixed that in this PR (just pushed, rebased on current main): host-fed bytes now stage at the generation level (bounded, discarded on overflow/stop/restart rather than truncated) and replay to the next decoder that opens, and the return contract is now "size whenever a CD stream is active, routed or staged; 0 only when inactive", with regression tests for feed-before-open, the staging cap, and stage discard. If what you found is something else, the details would be very welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants