Skip to content

About

The first Playdia emulator in the world

Resources

Stars

15 stars

Watchers

1 watching

Forks

Repository files navigation

PlayEm

PlayEm is an emulator for the Bandai Playdia (1994), an obscure Japanese FMV console.

Hardware

  • Console: Bandai Playdia Quick Interactive System (1994)
  • Main CPU: Toshiba TLCS-870 @ 8MHz
  • I/O CPU: NEC µPD78214GC (78K/II family) @ 12MHz
  • Video/Audio: Asahi Kasei AK8000 custom ASIC (no datasheet exists)
  • Video DAC: Philips TDA8772AH Triple 8-bit (CD-i compatible)
  • Media: CD-ROM XA Mode 2

Building

make            # standalone, SDL2 window
make libretro   # libretro core

Dependencies for the standalone: SDL2, libavcodec, libavutil, libswscale. Zips are read with the vendored miniz, so libzip is not needed.

The libretro core needs none of those - only a C compiler and libm. It is built from Makefile.libretro, which carries the platform handling:

make -f Makefile.libretro platform=unix        # also osx, ios, android, emscripten, win

Running

./playem game.cue              # Run with SDL2 window
./playem game.cue --headless   # Run without display (600 frames)
./playem game.cue --debug      # Run with per-second stats
./playem --test                # CPU self-test

Controls: Arrow keys = D-pad, Z (or Y on QWERTZ)/X = A/B, Enter = Start, Space = Select, F1 = Fullscreen, Esc = Quit

In the libretro core the same buttons are the RetroPad's D-pad, A, B, Start and Select.

libretro core

playem_libretro.so hands the frontend the 248×216 picture at 4:3, 30 fps and 44100 Hz, and takes .cue, .bin, .iso and Redump .zip by path. It sets need_fullpath, so it opens the disc itself; a path the C library cannot resolve - an Android SAF content:// URI - goes through the frontend's VFS instead, wrapped back into a FILE* (src/vfs_file.c).

Save states are not implemented: the emulator has no serialiser yet, so rewind, run-ahead and netplay are out until it grows one.

Emulator Status

Component Status
TLCS-870 CPU Substantial subset ported from MAME (BSD-3-Clause, David Haywood). Direct opcodes + e0/e8/f0/ff prefix families implemented.
NEC 78K/II CPU Substantial subset of the µPD78214GC ISA per the 78K/II manual (opcode encoding cross-checked against MAME's upd78k2d disassembler — MAME has no execution core).
CD-ROM CUE/BIN loading (single + multi-file), raw Mode 2/2352, ZIP support (miniz)
BIOS HLE Auto-scan for GLB/AJS, FMV playback loop
Video decode AK8000 proprietary DCT codec — 4×4 blocks, run/level VLC, DC prediction per macroblock (src/ak8000_pd.c)
Audio decode XA ADPCM with resampling to 44100 Hz
Interactive F2 commands — jumps, choices (F2 44/64), loops, quiz, timeout
Display SDL2 window, or a libretro core; 248×216 picture at 4:3
Pipeline 10 sectors/frame @ 30fps (~4× CD speed)

Interactive FMV Commands

The Playdia uses F2 sectors with submode 0x09 for interactive control:

Command Function Implementation
F2 40 Unconditional jump / scene loop Forward=auto-seek, backward=loop until button
F2 44 Player choice (7 button destinations) Wait for input, seek to chosen destination
F2 64 Timed jump with a player override Seeks by itself; a button held right then takes its own slot
F2 50 Quiz answer verification Wait for input, extra byte = correct/wrong flag
F2 60 Timed jump / animation Auto-seek with duration parameter
F2 80 Timeout handler Sets fallback destination for preceding F2 44
F2 90 Score/result display Auto-seek, sequential counter per button
F2 A0 Loop/animation control Flag F0 = boot marker (skipped)

Button mapping: B1=Start/default, B2=Up, B3=Down, B4=Left, B5=Right, B6=A, B7=B

MSF destination encoding: target_LBA = M×4500 + S×75 + F − 150 (binary, not BCD)

License

BSD 3-Clause — see LICENSE, which also lists the parts that come from elsewhere and keep their own terms: the AK8000 decoder ported from PlaydiaEmu (BSD 3-Clause), the TLCS-870 core ported from MAME (BSD 3-Clause), miniz (MIT) and the libretro API header (MIT).


AK8000 Video Codec — Reverse-Engineered Specification

The AK8000 uses a proprietary DCT-based video codec. No documentation exists anywhere — this was reverse-engineered from raw disc data across multiple games.

Disc Structure

  • F1 sectors: Video data (marker byte 0xF1, 2047 bytes payload)
  • F2 sectors: End of picture. The sector carries the tail of the picture from byte 0x23, then the assembled packet is decoded
  • F3 sectors: Scene marker (resets the accumulator). Filler - two bytes then 0xFF padding - is not a scene change and must not reset it
  • Each video packet: 6–13 F1 sectors, holding one picture

Picture Header (36 bytes)

Offset  Content
[0-2]   00 80 04          — 19-bit picture marker (0x400), then picture type
                            and quantizer shift
[3]     factor            — quantizer scale (observed: 4–40)
[4-19]  16-byte qtable    — luma quantization table
[20-35] 16-byte qtable    — chroma table (equal to the luma one on every game tested)

The entropy-coded rows start at bit 36 × 8; what earlier looked like a second marker at [36-38] is the first row marker of the picture.

The qtable is always 0A 14 0E 0D 12 25 16 1C 0F 18 0F 12 12 1F 11 14 — likely hardcoded in the AK8000 chip.

Image Format

  • Resolution: 248×216 pixels
  • Color: YCbCr 4:2:0 — 6 blocks per macroblock (4Y + Cb + Cr)
  • Macroblocks: 31×27 = 837 macroblocks, 5022 blocks total, 8×8 pixels each
  • Blocks: 4×4, not 8×8; each row of macroblocks opens with a 19-bit row marker
  • Y sub-block order: TL, TR, BL, BR within each 8×8 macroblock
  • DC prediction: the first luma block predicts the other three and the first luma block of the next macroblock; chroma predicts per component, reset each row
  • IDCT: integer 4×4 transform, pixel = IDCT(coeff × qtable × factor) + 128

Coefficient VLC

Run/level codes, 63 of them, from 2 to 13 bits, each followed by a sign bit. Two codes stand outside the table: 01 ends the block, and 001000 is the escape, carrying a 4-bit run and a 10-bit signed level. Coefficients are placed through the scan 0 1 4 8 5 2 3 6 9 12 13 10 7 11 14 15, and a run that pushes past the sixteenth coefficient is an error rather than something to clamp.

The table is in src/ak8000_pd.c; it is decoded through an 8192-entry lookup built at first use.

Dequantization

coeff × qtable[i] × factor, with the luma table for the four luma blocks and the chroma table for the other two, followed by the integer 4×4 inverse transform and +128. The trailer is at most 15 zero bits before byte-aligned 0xFF fill - anything else there means the picture is not intact.

XA ADPCM Audio

Standard CD-ROM XA ADPCM audio:

  • 18 sound groups × 128 bytes per sector
  • Each group: 16 bytes header + 112 bytes sample data (28 words × 4 bytes)
  • 4-bit ADPCM with 4 IIR filter modes
  • Coding byte: bit 0 = stereo, bit 2 = half rate (18900 Hz vs 37800 Hz)
  • Resampled to 44100 Hz via linear interpolation for SDL output

Test Games

All games share identical qtable values and frame header format.

Game Notes
Dragon Ball Z - Uchuu-hen Primary test game, QS=13/11/8 frames extracted
Aqua Adventure - Blue Lilty Different intro, same codec
Ultraman Powered Shares Bandai intro with DBZ
Sailor Moon S Shares Bandai intro with DBZ
Elements Voice Series Various titles tested

4/5 games share identical Bandai logo intro. QS decreases over intro: 13→13→13→13→11→10→10→8→8→7 (quality ramp-up).

What is still open

  1. Hardware pixel accuracy: the decoder produces the right picture, but the rounding of the inverse transform and the exact YCbCr coefficients have not been checked against hardware output.
  2. Eight pictures out of 39109 on Aqua Adventure fail entropy validation, in the same places PlaydiaEmu's own decoder fails. Whether the discs are like that or both decoders share a gap is unknown.
  3. Picture type and quantizer shift: the decoder only accepts type 1 with shift 0, which is all the tested discs carry. What else the fields can hold is unknown.
  4. F2 commands 0x88 and 0xB0 appear on discs and are not implemented; 0x64 is implemented from its layout rather than from documentation.

Reference Images

Real hardware screenshots in reference/ directory:

  • pd_real_qis.png — QIS boot screen
  • pd_real_suzu.png — Ie Naki Ko live-action FMV
  • pd_real_keroppi.png — Kero Kero Keroppi animation
  • pd_real_forest.png — Forest Sways title screen

CPU Cores

TLCS-870 (main CPU)

src/cpu_tlcs870.c is a C port of MAME's TLCS-870 core, originally written by David Haywood under the BSD-3-Clause license. The C port adapts MAME's C++ implementation to work without MAME's framework (no device_t, no address_space — operates directly on a uint8_t mem[64K] mirror provided by the host).

Coverage: all single-byte primary opcodes, plus the 0xE0..0xE7 source-prefix, 0xE8..0xEF reg-prefix, 0xF0..0xF7 dst-prefix, and 0xFF SWI families. Flag handling matches MAME's documented JF/ZF/CF/HF semantics with the spec's "JF = CF for ADD/SUB" rule (MAME's own implementation has a typo here; the comment in the source documents the correct intent).

Reset reads the vector at 0xFFFE..0xFFFF. Interrupts vector through 0xFFE0..0xFFFF (16 entries, priorities 0..15 with priority 15 being RESET). RBS-banked register file lives at 0x40 + RBS*8.

NEC µPD78214GC (78K/II I/O CPU)

src/cpu_nec78k.c is hand-written against the NEC 78K/II Instructions User's Manual, with opcode encoding cross-checked against MAME's upd78k2d disassembler (MAME has no execution core for this CPU family — upd78k2.cpp is explicitly tagged "Currently these devices are just stubs with no actual execution core.").

Implemented opcode families:

  • 1-byte direct ops: INC/DEC r, INCW/DECW rp, MOV A,r, XCH A,r, PUSH/POP rp, PUSH/POP PSW, DI/EI, RET/RETI/RETB/BRK, INCW SP / DECW SP, MOV [DE/HL +/-/=],A and inverse, CALLT [tbl]
  • ALU A,#imm / A,saddr / A,saddr_to_saddr / r,r' / AX,#word16 / AX,saddrp in all eight variants (ADD, ADDC, SUB, SUBC, AND, XOR, OR, CMP)
  • 16-bit ALU ADDW / SUBW / CMPW AX,#word16 / AX,saddrp / AX,rp
  • MOVW in all variants (rp,#word / sfrp,#word / saddrp,#word / AX,sfrp / AX,saddrp / sfrp,AX / saddrp,AX / rp,rp' / AX,[DE]/[HL] indirect)
  • Branches: BR$rel8 / BR!addr16 / BNZ/BZ/BNC/BC / DBNZ r / DBNZ saddr / CALLF !addr / CALL !addr16 / CALL rp / BR rp / CALLT [tbl]
  • Bit ops: SET1/CLR1/NOT1 CY / saddr.bit / PSW.bit / r.bit, MOV1/AND1/OR1/XOR1 CY,<bit> and inverse, BT/BF/BTCLR <bit>,$rel8 for saddr/sfr/PSW/r forms
  • Shifts/rotates: RORC/ROR/SHR/SHRW/ROLC/ROL/SHL/SHLW r/rp,n via the 0x30/0x31 prefix
  • Indirect addressing: [base+disp8] (DE/SP/HL), word[A] / word[B] indexed, word[DE] / word[HL] 16-bit-displaced, [DE+]/[HL+]/[DE-]/[HL-] auto-mod
  • MULU r / DIVUW r (78K/II extensions)
  • SEL RB0..RB3 register-bank switching
  • MOV STBC,#imm (write-protected SFR) drives HALT/STOP via bits 0/1
  • BRK / RETB (software interrupt + return)

Reset reads vector at mem[0x0000..0x0001]; PSW initialised to 0x02; SP starts at 0xFEDF. Register banks at 0xFEF8/0xFEF0/0xFEE8/0xFEE0 for RB0..RB3 per MAME's register_base() computation. CALLT table at 0x0040..0x007E.

Not yet implemented: the & (alternate-bank, prefix 0x01 0x05/0x06/0x09/0x0A/ 0x16/0x5x) expansion-memory forms. The 1 MB physical space is paged via the external MM SFR at 0xFFC4 (host responsibility — the CPU core itself only sees the current 64 KB window).


Bitstream Structure — SOLVED (2026-09-19)

The syntax was recovered by PlaydiaEmu and is implemented here in src/ak8000_pd.c. tools/pd_vcodec_test.c decodes every picture on a disc and matches PlaydiaEmu's own playdia-frame byte for byte.

Everything from here to the end of this file is the record of the search that did not find it, kept because the negative results are still worth something. It describes what was believed at the time, not how the decoder works now.

The dead end (2026-03-15)

The video bitstream encoding remained unsolved for six months. After extensive analysis including 600K+ brute-force VLC permutation tests, 225K structural parameter combinations, and comparison with multiple game-era codecs, no decode model produces recognizable images.

What IS known

  • Bitstream starts somewhere around bytes 40-44 of the packet
  • Each frame consumes approximately 28-33% of the packet bitstream → 3 frames per packet
  • The VLC produces values with a correct brightness distribution (QQ-correlation 0.96 with reference images)
  • AC sections within blocks show real DCT basis function patterns
  • Bytes 40-42 correlate with global frame color (Y, Cb, Cr)
  • The MPEG-1 luminance DC VLC decodes without errors (but this is trivially true for ANY bitstream since it's a complete prefix code)

What is NOT known

  • The correct VLC table (MPEG-1 DC VLC is assumed but NOT confirmed)
  • Whether DC and AC use the same or different VLC tables (PC-FX uses different tables)
  • The DC prediction model (DPCM, offset, 2D predictor, or other)
  • The AC coding model (run-level, sequential, fixed count)
  • The spatial block ordering (scan order)
  • The dequantization formula
  • Whether level shift (+128) is applied

Reverse Engineering Methodology

Exhaustive Search (2026-03-14 to 2026-03-15)

Over 1 million decode configurations tested including:

VLC tables: Standard MPEG-1 DC (sizes 0-16), modified size 7/8 (both 6-bit), 362K permutations of size assignments, H.261, MPEG-2, JPEG AC, Exp-Golomb, Rice coding

DC models: DPCM (continuous, per-row reset, per-MB reset), DC offset (init+diff), absolute, 2D predictor (avg left+above), vertical DPCM, various init values (0, 128, byte[40])

AC models: Sequential VLC 0=EOB, fixed count (7-63), 6-bit EOB + unary run + VLC level, combined run-level (|v|-1)/3, MPEG-1 B.14/B.15, no AC

Structural: Resolutions 128-288 width, 4:2:0/4:2:2, MB orders (row/col/zigzag/boustro + flips), Y block orders (std/col/rev), bit orders (MSB/LSB), start bytes (36-48), bit offsets (0-7)

Other codecs: PS1 MDEC, CD-i DYUV, Hadamard transform, DST, pixel DPCM, Cinepak-like VQ, 4-bit raw, 4×4 DCT sub-blocks

Correlation methods: Raw Pearson, 1D detrended (row means), 2D detrended (row+col means), Spearman rank, QQ-plot, cross-game matching, shuffle tests

Key Findings

  1. Format is 100% proprietary — no MPEG-1 start codes, no MPEG-PS pack headers, ffprobe recognizes no standard codec or container
  2. QIS boot logo appears on all 32 tested games — Group A (12 games, Y_init=127) and Group B (16 games, Y_init=144) with identical bitstreams within each group
  3. VLC value DISTRIBUTION matches reference (QQ-correlation 0.96) but spatial ordering does not — the brightness values are correct but placed at wrong 2D positions
  4. All high correlations proved to be DPCM drift artifacts — cumulative sums always create gradients that spuriously match reference images (1D, 2D, diagonal variants)
  5. VLC brute-force overfits — different packets produce different "best" VLC tables, confirming the matches are statistical coincidences, not the real VLC
  6. AC blocks show DCT basis patterns — horizontal/vertical gradients within 8×8 blocks consistent with DCT transform

Comparison with PC-FX (similar era proprietary FMV)

The PC-FX RAINBOW decoder (reversed by David Michel, implemented in Mednafen) uses:

  • Different VLC tables for DC vs AC and for luma vs chroma
  • DPCM DC prediction, run-level AC coding
  • Dequant: coeff × qtable[pos]
  • Y block order: TL, BL, TR, BR (not standard TL, TR, BL, BR)
  • Level shift +128

The Playdia AK8000 likely uses a similar DCT+VLC architecture but with unknown custom tables.

What's Needed to Solve

Nothing, as it turned out: the syntax came from PlaydiaEmu rather than from any of the approaches below. The list is kept as the record of what was tried.

  • Hardware capture: Record Playdia composite video output synchronized with disc sector reads to create ground-truth input/output pairs
  • AK8000 die shot: Decap the chip and analyze the decoder logic
  • Japanese community contact: Someone may have reversed this already (Twitter #レトロゲーム, #プレイディア)

See git history for ~80 test tools in tools/.

About

The first Playdia emulator in the world

Resources

Stars

15 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages