A state stored the whole 22 MB DRAM allocation -- sized for the
largest memory configuration and the high-resolution VRAM copies --
plus both ROMs, whatever the game used, and the DSP's 70 KB of decode
tables. V4 keeps everything else as it was and changes two chunks:
- MEM2 replaces MEM. It holds the memory configuration, then only the
RAM that configuration uses (DRAM, VRAM and VRAM's three high-
resolution copies), then NVRAM. The copies are kept whether or not
high resolution is on, because it can be switched mid-game and the
state size must not change under a frontend that sized its buffers
once; the configuration itself is fixed for a session. The ROMs are
left out: they are the BIOS and font loaded for the session and
never change. For the stock 2 MB/1 MB configuration that is 6 MB of
RAM instead of 22 MB, and 2 MB of ROM gone.
- DSP2 replaces DSPP. INSTTRAS, REGCONV and BRCONDTAB are decode tables
opera_dsp_init() builds and nothing changes, so they are not stored;
a load keeps the running core's.
V3 states still load: MEM and DSPP are still read, and the tables a
DSPP chunk carries are stepped over. A core without this change
refuses a V4 state as an unknown version.
Measured with a libretro driver, stock configuration, together with
the array I/O change before this:
size serialize unserialize
before 25550222 3.8 ms 4.2 ms
V4 6605970 0.5 ms 0.6 ms
Checked: a V3 state saved by the previous core loads and plays the
next 240 frames with audio and video identical to the original run; a
V4 state does the same; save-load-save is byte-identical; a replay of
60 frames from a loaded state matches; a sweep flipping 9721 bytes
never lets a refused load change the running state; and the changed
files build big-endian (PowerPC).
Saving or loading a state spent most of its time in the state
writer's and reader's per-element calls rather than in copying memory.
Every u8/u16/u32 went through write_bytes/read_bytes, which bounds-
checked and memcpy'd one to four bytes, and every array -- the CLIO
register file alone is 65536 words -- was written and read one element
at a time that way.
- Arrays are written and read with one bounds check and one copy. The
stored form is little-endian, so on a little-endian host that is the
array's own bytes; a big-endian host converts element by element.
- Scalars reserve their bytes and store them directly instead of
building them in a temporary and copying it in.
The bytes written are unchanged. A state saved by this core matches
one saved by the previous core except for the PRNG seed, which already
differs from run to run. Checked on little- and big-endian (PowerPC
under qemu) that the layout matches byte for byte and that overruns
are still refused. Run-ahead and rewind see the difference: serialize
went from 3.8 to 2.7 ms and unserialize from 4.2 to 3.0 ms.
Every state load first serialized the running state into a fresh
allocation the size of a state -- 25.5 MB -- so a failed load could be
rolled back. That doubled the cost of every load, and run-ahead and
rewind load every frame.
A current-format state is now checked first and then loaded. Each
current-format loader already parses its chunk into locals and checks
it before touching emulator state; with opera_state_set_validating()
set they stop there, so a state that validates is one the real load
accepts in full and there is nothing to roll back. In that pass:
- the MEM chunk's 25 MB payload is stepped over (sizes and config are
still checked), not copied;
- the xbus devices are not reset, and the CD-ROM device fields are
checked against a scratch copy;
- the memory-map sync, pbus reset and other side effects after loading
are skipped.
An older format, or a state carrying an older CD-ROM chunk, is not
validated and takes the old backup path, as does anything that fails
validation, so a refused state still leaves the running state as it
was.
Measured with a libretro driver over 200 loads: retro_unserialize went
from 9.08 ms to 4.20 ms on average (best 7.80 ms to 3.58 ms). Checked:
save, load and save again is byte-identical; a state loaded and run 60
frames matches the original run byte for byte; and a sweep flipping
each of 9721 bytes (every 64th byte of the first 360 KB, every byte of
the last 4 KB) is accepted or refused exactly as before, with no
refused load ever changing the running state.
Every sector read was a seek and a read through the VFS, and a CHD
copied each compressed hunk twice on its way to the decoder: from the
file into a 64 KiB request buffer, in pieces of at most 64 KiB, then
into the decoder's own staging buffer.
- Images are opened with RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS,
and HAVE_MMAP is now defined for unix, osx and Android, so the
local VFS maps them. A BIN/ISO/CUE sector read becomes a copy out
of the page cache into the drive's buffer, which is the one copy
the emulated drive needs.
- CHD: the compressed hunk is lent to the decoder straight from the
mapping (rchd_feed_borrow), so it is never copied. Unmapped (a
frontend VFS, a platform without mapping, a file too big to map),
each hunk is read in one call into a buffer sized to a hunk at open
and lent from there, rather than copied in again. The open sequence
is still fed by copy. The file position is tracked so a read that
continues where the last one stopped does not seek.
- On a mapped CHD the OS is asked to bring the next 1 MiB of the file
in ahead of the decoder (filestream_prefetch), coalesced so one
request covers many hunks; cold pages would otherwise stall the
emulation thread on a page fault.
- BIN/ISO/CUE: a read that starts where the previous one ended skips
the seek.
Measured over 20 passes of every sector of a 2400-sector test disc:
BIN/CUE went from 75593 read/lseek syscalls to 9 and from 0.57 to
0.22 us per sector; a CHD from 13392 syscalls to 26 (default hunks)
and from 1493 to 28 (1 MiB hunks). Unmapped, lseeks on a CHD dropped
from 6009 to 26 and 1 MiB-hunk reads from 984 to 264.
tools/chd/run.sh now also checks every codec at the largest hunk
chdman writes (1 MiB), and passes under ASan/UBSan on both the mapped
and the unmapped path.
Brings in RetroArch's "rchd: allocate nothing per hunk once an image
is open" and "rchd: say how long lent bytes have to live". CHD reads
no longer allocate once the image is open: the staging buffer is
reused, the CD FLAC decoder is kept and rewound, and the hunk cache,
CD sector scratch and FLAC rewind point are allocated at open. A
malloc counter over every sector of CD LZMA, CD FLAC and mixed images
went from 299 allocations during reads to 0.
zlib had two users left. The core inflated its built-in boot ROM with
it, and the test harness deflated RGB frames for the kitty terminal
preview. libretro-common's encoding_deflate already does both and the
core already built it for CHD, so deps/zlib-1.3.1.2 goes.
- opera_bios.c: inflate the boot ROM with rinflate (zlib wrapper,
adler32 checked) straight into the caller's buffer. Checked: the
ROM comes out at 131072 bytes with CRC32 dacb21ad, as opera_boot.h
records, under ASan/UBSan.
- encoding_deflate.c is now always built (it was CHD-only), and a
static build adds it with encoding_crc32 and features_cpu whether
or not CHD is on.
- test_harness.c: kitty preview compression through rdeflate. Output
checked to decode with zlib, including incompressible input against
the buffer bound.
- Makefile/Makefile.common: drop the zlib sources, SYSTEM_ZLIB,
DEPS_DIR and -I$(DEPS_DIR).
tools/embed_boot_rom.py still uses Python's zlib module to make
opera_boot.h; that is a build-time tool, not a dependency of the core.
Bring every libretro-common file the core builds up to RetroArch
61606d92b8b026106b51f75365bea592a6fb94b9. 34 files had drifted, including rchd (now accepts
version 5 hunks up to 1 MiB, the largest chdman writes) and
chd_stream, which now reads through vfs_prefetch.
New files the synced sources need: vfs/vfs_prefetch.c and
encodings/encoding_xxh64.c (with their headers), built with HAVE_CHD.
Dropped what nothing builds or includes any more:
streams/file_stream_transforms.c and its header (the core never calls
rfopen/rfread and friends) and the libchdr headers (the core always
reads CHDs with rchd).
tools/chd/run.sh: drop memmap/memmap.c, which the repo does not carry
(the script could not build), and add the new sources plus
retro_eventcount.c that vfs_prefetch needs with threads.
Crash 'n Burn (USA, Not for Resale) played character-selection speech ahead
of its video after loading the supplied state and pressing B.
The state retained input DMA channel 7's consumed-byte cursor at 8,112.
When the game programmed the next sample's current address as 0x002e753c,
Opera replaced the buffer base but left that cursor intact. Actual reads
therefore began at 0x002e94ec, skipping the first 8,112 compressed bytes of
the new speech sample.
The game's half-rate mono SDX2 stream consumes 22,050 compressed bytes per
second, so the skipped data advances speech by 367.891 ms. Its movie
player advances pictures from the graphics VBL counter independently of
the audio cursor, exposing the mismatch as lip-sync error.
Reset the selected input channel's cursor whenever its current DMA
address is written. Apply the register semantics to every input channel,
without a title-specific or savestate-specific workaround. Updating only
the next-buffer address still leaves the current transfer untouched.
Replaying the supplied state for 1,200 fields with the correction kept all
600 captured video frames unchanged and corrected the predicted audio
offset of 16,224 output frames at 44.1 kHz. The production replay matched
the diagnostic cursor-reset reference exactly in both pictures and PCM.
AutoBahn Tokio (Japan) could appear frozen at the end of its unattended
attract race. Its animation advances by counting rendering iterations,
while a separate attract timer determines when the sequence ends.
Instantaneous CEL completion let the animation finish too early, then
repeatedly redraw its final scene until that timer expired. The expensive
redraws made the remaining hold especially slow in wall-clock time.
Keep STATBITS/SPRON busy for an estimated rendering interval after each
list is drawn, so software polling CEL completion cannot submit lists
without consuming emulated rendering time. Estimate work from the greater
of evaluated source texels and written destination pixels, at roughly
9 million work units per second, plus 64 CPU cycles per fetched CCB.
AutoBahn's surface-mapped lists evaluate substantially more source texels
than they write pixels; counting only writes still finishes its animation
too early.
The observed attract sequence now continues its ending animation through
the credits and fade and returns to the title screen. Native and HiRes
rendering share native-resolution timing accounting, and state loading
preserves whether a list still needs drawing or is only waiting for its
busy deadline. Legacy states retain their pending-list convention.
This also changes pacing in Casper and other games that depend on CEL
completion instead of an independent frame limiter. Casper comparisons
showed a reduced render/present cadence while video refresh remained
constant; that is a broader timing impact, not a separately established
Casper bug fix.
The busy interval is an approximation. Emulator scene progression and
pacing observations do not establish hardware-calibrated CEL timings.
Wing Commander III - Heart of the Tiger (USA, Disc 1) could stall during
its credits or opening movies because streaming completion became visible
at the wrong boundaries.
Publish successful MEI READ_DATA status only after the final requested
byte has left the data FIFO. Reporting success as soon as one sector is
buffered prematurely ends Portfolio's read-ahead command, even while data
from the request remains unread.
Raise CLIO input-DMA completion and reload the next buffer when the final
sample is consumed, rather than waiting for another DSP read. Wing
Commander III can otherwise observe the end address and detach an audio
attachment whose completion has not fired. Once the instrument stops,
there is no further read to generate completion and playback waits
forever.
During investigation, stock-2x playback advanced through the previously
stuck credits and opening movie sequence to the interactive hangar. This
addresses the observed streaming stalls, not an exhaustive compatibility
claim for every gameplay or disc-transition path.
Also default CD-ROM speed to the stock 2x rate in both option definitions
and the missing-option fallback. This affects all titles using the
default; explicit unbounded and other saved speed selections remain
available and unchanged.
Store the ROM as a compressed static blob and load it directly into
ROM memory. Keep Opera Boot first and default in BIOS settings while
preserving external BIOS selection and load errors.
- Boot ARM at address zero through a ROM overlay cleared by CPU DRAM
writes; preserve overlay state with backward-compatible save loading.
- Distinguish power-on, software, and DIPIR resets; preserve hardware
state and interrupted ARM context, including the SWP return address.
- Latch XBUS media access in CLIO, enforce DIPIR protection, resume
pending DMA when access returns, and derive enabled interrupt summaries.
- Separate CD-ROM media load/eject from device reset, model local reset
transitions, and defer ODE media access until the launch response drains.
- Let the boot ROM initialize the display; remove synthetic boot-memory
and startup VDL seeding.
The CUE FILE entry is joined onto the directory of the content path with
snprintf, and every copy into the fixed STRING_MAX buffers goes through
strlcpy/strlcat, so an over-long path or FILE line truncates instead of
overrunning the stack.
Nothing in the core calls into memmap.c: file_stream maps files through
vfs_implementation, which takes mmap() from the system headers. The
synced copy carries memshm_create, which needs shm_open/shm_unlink from
librt on glibc older than 2.34, so linking it with --no-undefined fails
on the xenial build lanes.
The threaded DSP takes no mutex or condition variable. The emulation
thread publishes a monotonic tick count with a release store and wakes
the DSP thread through a retro_eventcount once per 32 ticks and at
frame end; the DSP thread runs every posted tick and writes samples
into a retro_spsc ring that upload hands to the frontend in place.
Mode switches keep samples in order, and the thread runs out every
posted tick before it exits. THREADED_DSP defines HAVE_THREADS and
builds the eventcount and SPSC sources.
libretro-common is synced with RetroArch master and holds only the
sources the core builds and the headers they and the core include;
rstrtod.c is built since file_stream needs it.
Address both VRAM banks and correct SPORT mask semantics, including
high-resolution plane preservation. Validate configured and saved
memory layouts, and propagate allocation failures through core
initialization and reset.
Keep the 22 MB backing allocation and state formats unchanged. Add
memory regressions and document panafz1 boot compatibility without
patching the BIOS.
The CHD build selects rchd's zstd codec alongside DEFLATE, LZMA and
FLAC and compiles encoding_rzstd.c, so images compressed with cdzs or
zstd read like any other. rchd, chd_stream, encoding_rzstd,
encoding_deflate and compat/intrinsics.h are resynced from RetroArch.
tools/chd/run.sh compresses a synthetic two-track image with each CD
codec chdman offers and checks every sector against the CUE it came
from, raw and cooked, under ASan and UBSan.
The CHD path adds features_cpu.c to SOURCES_C whenever
STATIC_LINKING is set, and on _3DS that translation unit reaches
for <3ds/svc.h>, <3ds/os.h> and <3ds/services/cfgu.h> for
svcGetSystemTick, osGetTime and CFGU_GetSystemModel. Nothing else
in the core touches a libctru header, so the ctr block never
carried an include directory for one and the compile stops at the
first #include.
Point INCFLAGS_PLATFORM at $(CTRULIB)/include, defaulting CTRULIB
to $(DEVKITPRO)/libctru so the target builds both under
libretro-super, which exports CTRULIB, and in a bare devkitARM
container, which only exports DEVKITPRO.
- Refresh the vendored libretro-common tree to the pinned upstream snapshot.
- Replace CHD stream integration with VFS-backed RCHD decoding and per-track logical maps.
- Build the RCHD and RFLAC codec closure and remove obsolete libchdr and LZMA dependency trees.
- Negotiate frontend VFS v3 for file, path, and directory access with legacy fallback.
- Preserve mixed-track gaps, raw-sector offsets, and native CD-DA sample order.
- Compute the source width before applying projection state.
- Treat SKIPX values that exhaust the span as empty draws.
- Preserve height-based CCB position advancement for skipped spans.
Add image-protocol terminal renderers to the test harness with Kitty
and Sixel capability probes, renderer metrics, frame caching, and
resize/exit cleanup.
LRFORM describes the interleaved row-pair memory layout used by Opera
bitmaps. Adjacent vertical pixels are stored together, so literal
LRFORM CELs have VCNT values that count row pairs and source reads
that alternate between the two halfword lanes.
Although this layout is most commonly used for 16-bpp bitmap
extraction, games can apply lower-depth literal CELs to LR-format
bitmap-derived data. AITD2 uses a 1-bpp LRFORM priority mask to select
where foreground scenery is copied back over actors. Treating LRFORM
as 16-bpp-only leaves that mask at half height and decodes the wrong
row lane for the lower half.
The bottom two bits of the address bus aren't even wired up to
MADAM. Most titles use properly aligned addresses but not all. This
patch ensures the pointers read from the CCB and the CCB address
itself are masked to align them to word boundry.
Fixes Ballz and possibly other titles that had unaligned addresses.
- Determine decoder transparency before blue-LSB substitution.
- Use decoded PDC multiplier bits for PDC_MFONLY PIXC processing.
Fixes text rendering in early credits scene in Alone in the Dark.
- Remove the FIX_BIT_TIMING_6 flag and LR CEL framebuffer-read branch.
- Drop the opera_hack_timing_6 core option from v1 and v2 definitions.
- Stop parsing hack_timing_6 into the emulator hack flags.
- Correct CEL XYPOS, LINEDXY, and DXY register mapping.
- Retain and advance CEL origin for YOXY=0 chains.
- Remove the graphics_step_y compatibility hack option.
Fixes Samurai Shodown.
Arbitrary CEL rendering treated computed horizontal span endpoints as exclusive. When adjacent skybox CELs met on integer x boundaries, neither CEL covered the shared column, leaving the SPORT clear color in the frame buffer and producing vertical seams in NFS.
Include the right endpoint before clipping arbitrary CEL spans so shared boundaries are overwritten by CEL pixels instead of preserving stale clear-color columns.
CPU writes to EI FIFO-head addresses 0x70..0x7c set CPUSupply and latch the
supplied word in EIRAM. When the DSP later reads the normal input FIFO window
at 0xf0..0xfc, consume that latched word instead of returning PRNG data.
The random source is the separate DSPP noise register. Using it for a valid
CPU-supplied FIFO read injects spurious samples, causing startup pops in
Flashback sound effects such as footsteps.
Source:
- WO 94/16383, PDF pp.17-18: 0EAh is the pseudorandom noise generator, 0F0h
through 0FCh are input FIFOs, and 070h through 07Eh read the corresponding
input FIFOs without removing the input word.
- WO 94/16383, PDF pp.25-26: EI FIFO reads refill EIRAM from the corresponding
FIFO, and the EI FIFO control unit also receives direct host CPU input.
- Portfolio OS src/audio/audiofolio/dspp_addresses.h: DSPP_FIFOHEAD_ZERO_ADDRESS
is 0x0070 and DSPI_NOISE is 0x00EA, separating FIFO-head data from the noise
register.
- Portfolio OS src/audio/audiofolio/dspp_touch_anvil.c: dsphResetFIFO() clears
DSPP_FIFOHEAD_ZERO_ADDRESS + DMAChan in EI memory to reduce pops.