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

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 once the active stream is routed to
the open decoder(s) — regardless of how many decoders receive it — and 0
when no CD stream is active.

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

Three added tests: 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. Each pinned
property has a one-line mutation that makes exactly one named test fail:

  • Return reports the input range once, not once per decoder: in
    feedMpegCdStreamBytes, change return routedCount != 0u ? size : 0u; to
    return routedCount * size; — run the suite; exactly "host feed fans out
    to every open decoder and reports the input range once" fails.
  • Fan-out reaches every open decoder: in feedMpegCdStreamBytes, add a
    break; right after ++routedCount; — run the suite; exactly "host feed
    fans out to every open decoder and reports the input range once" fails.
  • Active-gate clause A (a stream must have started): in
    feedMpegCdStreamBytes, drop cdStreamGeneration != 0u && from the
    cdStreamActive expression — run the suite; exactly "feedMpegCdStreamBytes
    before a CD stream start is a no-op" fails.
  • Active-gate clause B (stream not yet ended): in feedMpegCdStreamBytes,
    drop && !...currentCdStreamEofSeen from the cdStreamActive expression —
    run the suite; exactly "host-fed CD stream decodes without a guest demux
    call" fails (the feed-after-EOF assertion).
  • Guest demux path still records addresses (no collateral damage): in
    appendPssBytes, change the default bool trackGuestAddrs = true to
    = false — run the suite; exactly "sceMpegDemuxPssRing dispatches
    registered video and audio stream callbacks" fails.

Note on the return value: it is size (the whole input range) once the
active stream is routed, or 0 otherwise — it is not a decoded-byte count
and does not scale with how many decoders are open.

Add feedMpegCdStreamBytes, a host-memory counterpart to the guest demux
syscalls, so runtimes that service the movie stream host-side (an HLE'd IOP
streamer and/or an on-disc stream transform) can push elementary-stream bytes
into the existing guest-driven decoder. Routes to the playback state(s) on the
current CD-stream generation, bracketed by notifyMpegCdStreamStart/Eof, and
demuxes through the same PSS path as sceMpegDemuxPss. Stream callbacks are not
dispatched on this guestless path (documented TODO for a follow-up queue).
Pure addition: no existing call site or behavior changes.
@smmathews
smmathews marked this pull request as ready for review July 21, 2026 18:49
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.

1 participant