15 KiB
Platform Portability
Living inventory of operating-system dependencies in the reimplementation and the work needed for future non-Windows builds. This is a tracking reference, not a commitment to expand the current Phase A slice. Native-engine reverse engineering remains Windows-oriented because the original game is a Windows program; that does not by itself make the authored runtime Windows-only.
Current portability boundary
The VM and content pipeline are already mostly platform-neutral:
Age.Engineuses managed .NET for script loading, bytecode execution, SYS4 catalog parsing, bounded ALF/AAI reads, LZSS, AGF-to-RGBA8 decode, and retained graphics state.- Godot owns ordinary graphics presentation, input, BGM, voice, and SFX playback.
- Effectful bytecode operations cross
Hosting/IHost.cs; the VM does not call native OS APIs. - Movie payloads arrive from
IAssetStoreas owned bytes and decoded frames enter the compositor as the platform-neutralRgbaImagetype. - Native-compatible persistence uses managed streams behind
INativeDatStore; Godot redirects AGE's engine-owned root touser://SAVEwhile retaining its fixed portable DAT/STH filenames.
The selected movie path now uses the project-owned FFmpeg C ABI rather than a Windows multimedia API, but only a Windows-x64 native bundle is built and staged today. The retired-live DirectShow implementation remains in-tree until the corpus and manual gates pass. There are also softer Windows assumptions that should be tested or replaced before claiming portable exports.
Dependency inventory
| Area | Current dependency | Runtime impact | Portability status / future action |
|---|---|---|---|
| Retained graphics presentation | Backend-neutral GfxState; accepted default Godot Sprite2D GPU stage plus the retained software pixel oracle, using runtime ImageTexture, canvas transforms/materials, and no native graphics API |
GPU backend caches static/color-key variants, updates dynamic surfaces, handles retained range transitions, and falls back whole-frame for the legacy host screen-transition path | Godot owns D3D/Vulkan/Metal/OpenGL selection; validate shader/blend/filter behavior per target rather than adding a platform renderer |
AGE movie decode (0x236 scene movies; 0x20f modal LOGO/OP/ED) |
FfmpegMovieDecoder is the selected live factory over the project-owned native/age_movie_ffmpeg ABI; DirectShowMovieDecoder remains unselected pending deletion |
Windows-x64 passes the complete 213-payload installed-corpus gate; other native targets and the remaining windowed CHAPTER/combat checks are pending | Run the windowed live gates, then delete DirectShow and add target-specific native builds |
| Movie integration | Each surface owns a unique playback-instance id; MovieRuntime owns IMovieDecoder from an injected factory, and the FFmpeg worker paces PTS against a monotonic clock with cancellation/failure completion |
Concurrent/restarted uses of one asset have independent frame/completion/teardown state; Main remains annotated Windows because only the win-x64 bundle is available |
Recheck the corrected combat lifecycle, then add Linux/macOS builds and remove the Windows annotation after DirectShow is deleted |
| Movie audio | FFmpeg detects the audio stream but the current ABI returns video frames only | MPEG movie audio remains intentionally silent | Extend the ABI with timestamped PCM and select an audio/presentation clock; separate feature slice |
| ADV font discovery | godot/Main.cs probes C:/Windows/Fonts for Japanese fonts |
Harmless fallback today, but appearance depends on host fonts | Bundle/configure a redistributable font or add platform-specific discovery |
| Filesystem semantics | Several filename and containment comparisons use OrdinalIgnoreCase; installed assets are conventionally uppercase |
Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
| Save/profile storage | Managed DirectoryNativeDatStore under Godot user://SAVE; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails |
No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target |
| Install/repository discovery | engine/Age.Engine/Sys4/Paths.cs finds age-reimpl above AppContext.BaseDirectory and assumes the current workspace sibling layout |
Suitable for development, not packaged exports on any OS | Replace runtime discovery with a user-selected game root/profile; retain repository paths only for developer tools/tests |
| Archive parity oracle | One integration test launches bin/BinExtractALF.exe |
Windows-only test helper, not a shipped runtime dependency | Skip/replace on non-Windows CI; runtime ALF/AAI readers do not depend on it |
| Native RE tools | Frida/Ghidra helpers target the original AGE.EXE; supporting utilities include Windows executables and Windows command conventions |
Development/research only | Keep separate from export requirements; document platform prerequisites per tool |
| Python workflow | Operating guide uses Windows py -3.11 invocation |
Developer workflow only | Add equivalent python3 instructions if non-Windows development becomes active |
No authored runtime code currently calls native DirectSound, Direct3D, Vulkan, Metal, or OpenGL. Mentions of
those APIs in docs/engine-re.md describe the original AGE implementation. Both the software-oracle display
and retained GPU prototype use Godot abstractions, so accepting the GPU backend does not add an OS graphics
dependency.
Movie backend replacement seam
The live connection is now backend-neutral:
VM op 0x236 (non-modal) / op 0x20f (modal)
-> IHost.PlayMovieToSurface / PlayModalMovieToSurface
-> VFS-owned MoviePayload bytes
-> IMovieDecoderFactory
-> FfmpegMovieDecoder (current live selection)
-> FfmpegMovieSession -> age_movie C ABI
-> DirectShowMovieDecoder (unselected; retained only through acceptance)
-> newest RGBA frame
-> retained movie surface
-> Godot compositor
Everything before and after the selected decoder is portable. The replacement decision is an in-process
FFmpeg backend behind a project-owned C ABI, not raw FFmpeg structs in Godot/C# and not a subprocess. FFmpeg
n8.1.2-29-g703dcc25b9 is pinned by immutable release URL and SHA-256 in
native/age_movie_ffmpeg/dependency-win64.json; changing that pin requires rerunning the full installed-movie
gate. The shim dynamically links an LGPL build made without GPL or nonfree components and
uses only libavformat, libavcodec, libavutil, and libswscale for the video slice. libswresample and the
MPEG audio decoders may be packaged now, but PCM delivery remains a separate slice. Release artifacts must carry
the matching FFmpeg source/configuration and notices required by FFmpeg's
license checklist.
The boundary has two layers:
- A small native
age_movieABI owns all FFmpeg objects and version-sensitive calls. It accepts a borrowed byte span only for the duration ofopen, copies it into native-owned memory, creates a seekable customAVIOContext, probes the MPEG program stream, and returns immutable video metadata. Sequential decode returns one top-down tightly packed RGBA8 frame plus its normalized presentation timestamp. Conversion occurs in an FFmpeg-owned aligned frame; only exact visible row bytes are copied into the caller's tightly packed buffer. Return statuses distinguish frame, EOF, invalid arguments, undersized output, and decoder failure; the open call returns a bounded UTF-8 diagnostic and an opened handle retains its last decode error. Close accepts a null handle, and managedSafeHandleownership guarantees one close for each successfully opened handle. No FFmpeg pointer crosses the ABI. - Managed
IMovieDecoderowns the native handle and the paced worker. Synchronous construction providesStopTimeMsbefore0x236returns. The worker uses a monotonic playback origin, decodes ahead by at most one frame, publishes frames when their timestamps become due, and retains newest-frame-wins behavior if Godot is late. EOF becomes completion only after the final frame's presentation interval/stop time, so surface cleanup cannot erase the last frame immediately. Disposal interrupts waits and joins without depending on Godot's main thread.
StopTimeMs performs a bounded packet scan over the seekable in-memory payload and takes the longest usable
FFmpeg format duration, video-stream duration, timestamp span, or constant-frame-rate packet-count duration. This
accounts for the final MPEG presentation interval that FFmpeg's stream duration can omit (MVB908 is ten 30 fps
frames: 333 ms, not the raw stream field's 316 ms). The result is converted to integer milliseconds with the same
positive truncation used by native op 0x23f, then the demuxer rewinds before playback. Streams with no usable
duration evidence fail initialization for the stock-content path rather than inventing timing. The existing host
safety rule still converts any backend initialization failure into an
explicitly completed zero-duration movie identity, and the presentation watchdog remains a last-resort guard for
an initialized backend that never reaches EOF.
The factory is injection for tests and future decoder replacement, not runtime codec roulette. FFmpeg is now the
selected Windows-x64 live backend; once its corpus and live gates pass, DirectShow is deleted rather than shipped
as a fallback. Modal
completion/cancel remains owned above the decoder by the existing 0x20f host path. A future audio implementation
will consume timestamped PCM and may become the presentation clock; it must not change the VM-facing stop-time,
surface, or cancellation contracts.
This replacement is now also required for Windows gameplay parity. Archive-backed probes on 2026-07-21
show that the current DirectShow graph accepts tested MPEG widths divisible by 16 (208, 288, 304, 400, and
800) but fails Connect with 0x80040217 for tested widths that are 8 mod 16 (280, 360, 520, and 600).
The dominant combat-effect family is 280x352 (125 installed MVB assets), so retaining DirectShow as the
only Windows decoder is insufficient even though SC0000's 800x600 CHAPTER.AGF works.
An archive scan on the same date classified all 213 installed movie payloads as MPEG-1 program streams with
MPEG-1 video and no MPEG-2 sequence extensions. Of those, 184 are video-only and 29 contain MPEG audio. The
large LOGO/OP/ED family uses MPEG Audio Layer II, while 25 small 120x120 MVS movies use Layer I. That inventory
is why the narrower pl_mpeg library was rejected as the primary backend: it fits every installed video stream
but only decodes Layer II audio and explicitly ignores program-stream PTS in its high-level synchronization.
FFmpeg covers the complete installed codec set and leaves the mod/profile boundary open without selecting a
different decoder per effect.
Native deliverables are RID-specific and bundled with the Godot export; the runtime must not discover an
arbitrary system FFmpeg. The first implementation gate is Windows x64 because that is the current runnable
target, but the C ABI and loader paths must reserve Windows x64, Linux x64, macOS x64, and macOS arm64 from the
start. Builds use shared libraries, $ORIGIN/@loader_path-style local lookup on Unix targets, recorded source
hashes and configure arguments, and no committed original-game data. Packaging automation is part of completing
the backend, not a prerequisite for the first native decode spike.
The Windows-x64 spike is now complete. bootstrap-win64.ps1 verifies the immutable archive SHA before
extraction, and build-win64.ps1 builds the shim with MSVC and places the DLL, import artifacts, required LGPL
shared libraries, and license under disposable build/native/win-x64. The managed resolver accepts
AGE_FFMPEG_NATIVE_DIR for the isolated gate and otherwise reserves application-local and
runtimes/<rid>/native lookup. Representative VFS results are MVB961 280x352/500 ms, MVB238
280x352/866 ms, MVB908 400x400/333 ms, and CHAPTER 800x600/12016 ms, all with changing frames and
nondecreasing timestamps. Malformed input and repeated teardown are covered. This is not yet distributable
packaging. FfmpegMovieDecoder now adds cancellable timestamp pacing and is the live selection. The Godot build
copies the shim, five required shared libraries, and FFmpeg license beside Himegari.dll; a natural SYSTEM4
smoke completed 7288 ms LOGO.AGF, opened 106919 ms OP.AGF, and published frames from both without loading
DirectShow; the user subsequently confirmed both opening movies work in normal windowed playback. Full
export/source-offer packaging and non-Windows builds remain outstanding.
The Windows-x64 installed-corpus gate passed on 2026-07-22 with the pinned dependency: all 213 signature-selected
MPEG payloads opened and reached EOF under a 30-second per-item bound. It decoded all 15,788 frames across twelve
display sizes (120x120 through 800x600), independently matched sequence-header dimensions, found positive
duration and frame-rate metadata, verified tightly packed RGBA size and nondecreasing timestamps, observed
changing imagery in every asset, and closed every session. The set includes all 184 video-only and 29 audio-bearing
streams; audio presence is detected but PCM remains intentionally undelivered. The 263-second ED.AGF was the
slowest decode at 4.4 seconds in the deliberately unpaced gate, so its prior 30-second failure was test-harness
whole-frame hashing overhead rather than a decoder incompatibility. The disposable machine report is
build/movie-corpus-ffmpeg.json; invocation and report semantics are canonical in docs/tools-reference.md.
Cross-platform validation gates
Before advertising a platform as supported:
- Build and export the Godot C# project for that target without compiling an unusable native backend.
- Run the platform-neutral engine tests, with Windows-only parity oracles explicitly classified.
- Exercise SYS4/AAI/ALF loose override precedence on a case-sensitive filesystem.
- Decode representative raw/compressed AGF variants and compare RGBA output with established fixtures.
- Play SC0000 through texture, BGM, voice, SFX, and movie publication using only the original archives.
- Verify Japanese font discovery/rendering and user-selected game-root handling outside the repository.
- If movie audio is implemented, validate A/V synchronization, interruption, EOF, and teardown separately from video visibility.
Update rule
Add an entry whenever authored runtime code gains an OS API, native library, platform-specific path, filesystem assumption, conditional build rule, or platform-specific test oracle. Record whether it affects the shipped runtime, only development tooling, or only native-engine research. Cross-link detailed subsystem semantics to their canonical document instead of duplicating them here.