Developer Guide¶
This guide is for working on the 5DO source. It maps the solution, explains the runtime and invariants, lists extension points, and records the conventions and known architectural debt. For the design rationale, see Architecture.md; for porting rules, see PortingNotes.md; for tests, see TestStrategy.md and BuildAndTest.md.
1. Source tree¶
src/
FiveDo.Core/ contracts, value types, constants, state primitives, logging
FiveDo.Cpu/ ARM60 interpreter, decoder, ALU, shifter, disassembler, SWI HLE
FiveDo.Memory/ DRAM/VRAM, ROM banks, NVRAM, CPU address bus
FiveDo.Madam/ registers, CEL engine, pixel decode/process/project, matrix
FiveDo.Clio/ registers, timers, FIFOs, DMA, interrupts, XBUS/DSP windows
FiveDo.Dsp/ DSP core, instruction decode, ALU, multiplier, memory
FiveDo.Sport/ VRAM fill/copy engine
FiveDo.Video/ VDLP, frame assembly, mailbox, CPU presenter, viewport
FiveDo.Video.OpenGL/ OpenGL/GLES presenter, shaders, filters, swap interval
FiveDo.Video.Vulkan/ optional Vulkan presenter, Win32 swapchain surface, SPIR-V shaders
FiveDo.Xbus/ XBUS controller and device slots
FiveDo.Cdrom/ MEI CD-ROM device
FiveDo.Storage/ ISO/BIN/CUE/CHD, BIOS/NVRAM, game database, prefetch
FiveDo.Audio/ ring, WAV, OpenAL, limiter, tap, analysis
FiveDo.Input/ controller state, PBUS, profiles, scripts, gamepad mapper
FiveDo.Diagnostics/ PNG/frame dumps, tracing, timing, benchmark gates
FiveDo.Machine/ composition root, frame loop, Quartz scheduler
FiveDo.Cheats/ per-game cheat parser, engine, and JSON document store
FiveDo.Tas/ TAS movie container, record/replay, branching, verification
FiveDo.Host/ core runner, pacing, settings, slots, library, fixes, autopilot, cheats
FiveDo.Cli/ headless commands and JSON contracts
FiveDo.Frontend.Avalonia/ desktop shell, video surface, dialogs, GPU probe
tests/FiveDo.Tests/ xUnit suite, one folder per subsystem
benchmarks/FiveDo.Benchmarks custom frame benchmark and CI gate
packaging/ bundle/archive scripts and Linux desktop entry
tools/ release soak, collection compatibility sweep, 4DO reference/result generators, HQx generation, differential harness
docs/ this documentation set
2. Dependency graph¶
FiveDo.Frontend.Avalonia ---> FiveDo.Host
| |
+--> FiveDo.Video.OpenGL +--> FiveDo.Machine
+--> FiveDo.Cheats
+--> FiveDo.Tas
+--> FiveDo.Storage
+--> FiveDo.Audio
+--> FiveDo.Input
+--> FiveDo.Video
+--> FiveDo.Diagnostics
+--> FiveDo.Core
FiveDo.Cli ----------------> FiveDo.Host
FiveDo.Machine ------------> Core, Cpu, Memory, Madam, Clio, Dsp, Sport,
Video, Xbus, Cdrom, Diagnostics
FiveDo.Video.OpenGL -------> Core, Video
FiveDo.Storage ------------> Core (+ CHDSharp)
FiveDo.Audio --------------> Core (+ Silk.NET.OpenAL)
FiveDo.Input --------------> Core
FiveDo.Cheats ------------> Machine, Input
FiveDo.Tas --------------> Core, Input
FiveDo.Video --------------> Core
FiveDo.Diagnostics --------> Core
FiveDo.Memory -------------> Core
FiveDo.Cpu ----------------> Core
FiveDo.Madam --------------> Core
FiveDo.Clio ---------------> Core
FiveDo.Dsp ----------------> Core
FiveDo.Sport --------------> Core
FiveDo.Xbus ---------------> Core
FiveDo.Cdrom --------------> Core
Rules:
- Arrows point downward only.
FiveDo.Corehas no project references. - No chip references another chip; they communicate through Core interfaces and are wired by
FiveDo.Machine. - The core is platform-neutral: no UI, file I/O, wall clock, or native interop in
Core/chips.FiveDo.Hostand frontends own the outside world. FiveDo.Video.OpenGLconsumesVdlFrameonly; it never touches emulated state.- The CLI and desktop both consume
FiveDo.Host; neither is referenced by the core.
3. Project catalog¶
| Project | Responsibility | Key types | References |
|---|---|---|---|
FiveDo.Core | Contracts, shared types, constants, fix mode, logging, state primitives | IAddressBus, IRam, ICpuBackend, IInterruptSink, IFrameSink, IAudioSink, IPbusSource, IDiscSource, IXbusDevice, IVdlp, IMadamBus, VdlFrame, VdlLine, FrameBuffer, FixMode, EmulatorTuning, IStateReader/Writer, StateFormat, StateSectionIds | none |
FiveDo.Cpu | ARM60 interpreter, optional managed IL JIT, decoder, ALU, shifter, disassembler, Portfolio OS SWI HLE | Arm60, Arm60State, ArmDecoder, ArmAlu, ArmBarrelShifter, ArmDisassembler, ArmSwiHle | Core |
FiveDo.Memory | RAM/ROM/NVRAM, word-swapped representation, mirrors, bus decode | FiveDoMemory, Ram, RomBank, Nvram | Core |
FiveDo.Madam | MADAM registers, CEL engine, pixel decoder/processor/projector, matrix, PLUT, PBUS | Madam, CelEngine, CelBitReader, PixelDecoder, PixelProcessor, Projector, CelRasterizer, CelGeometry, CcbParser | Core |
FiveDo.Clio | CLIO registers, timers, FIFOs, DMA, interrupts, DSP interface, XBUS window, ROM select, diag port | Clio, DiagPort | Core |
FiveDo.Dsp | DSP instruction decode/execute, ALU, multiplier, NMem/IMem, semaphore, sample output | Dsp, DspInstruction | Core |
FiveDo.Sport | VRAM fill/copy, high-res plane mirroring | Sport | Core |
FiveDo.Video | VDLP display-list parser, line/frame assembly, CPU oracle, mailbox, viewport, CPU fallback, shared HQx CPU filter | Vdlp, FrameAssembler, GpuPalette, VdlFrameUploader, VdlFrameMailbox, ViewportCalculator, CpuVideoRenderer, RendererSettings, IVideoRenderer, HqxFilter | Core |
FiveDo.Video.OpenGL | Desktop GL/GLES presenter, two-stage palette/scale, filters, shaders, swap interval | OpenGlRenderer, OpenGlCapabilities, SwapInterval, Shaders, GlContextAdapter, OffscreenRenderTarget | Core, Video, Silk.NET.OpenGL |
FiveDo.Video.Vulkan | Optional offscreen Vulkan 1.0 presenter, build-time SPIR-V palette/scale/xBR/FSR shaders, accurate/nearest/sharp-bilinear/CRT/xBR/FSR/HQx, readback | VulkanOffscreenRenderer, generated VulkanShaders | Core, Video, Silk.NET.Vulkan |
FiveDo.Xbus | XBUS controller, device slots, poll/status/data FIFOs | Xbus | Core |
FiveDo.Cdrom | MEI CD-ROM command state machine, TOC/MSF/LBA, sector streaming | CdromDevice | Core |
FiveDo.Storage | ISO/BIN/CUE/CHD readers, prefetch, BIOS/NVRAM, volume header, game ID/database | IsoImage, BinImage, CueSheet, ChdDiscImage, PrefetchingDiscSource, DiscImageFactory, BiosImage, NvramImage, VolumeHeader, GameId, GameDatabase, GameDatabaseScanner, GameDatabaseNameParser | Core, CHDSharp |
FiveDo.Audio | Ring buffer, WAV capture, OpenAL output, soft limiter, tap, analysis | AudioRingBuffer, AudioPipeline, WavAudioSink, OpenAlAudioOutput, AudioSoftLimiter, AudioTap, AudioAnalyzer, AudioStatistics | Core, Silk.NET.OpenAL |
FiveDo.Input | Controller state, PBUS encoding, profiles, scripts, keyboard/gamepad mapping, input recording | ControllerState, PbusEncoder, InputProfile, KeyboardInputMapper, InputManager, GamepadMapper, InputScript, InputRecorder | Core |
FiveDo.Diagnostics | Logging, bug reports, launch statistics, donation verification, PNG/frame dumps, CPU tracing, timing, benchmark gates | BugReportSink, ApplicationStatsClient, DonationVerificationClient, PngDumper, FrameDumper, CpuTracer, FrameTiming, BenchmarkGate, BenchmarkComparison | Core |
FiveDo.Machine | Composition root, frame loop, Quartz scheduler, state sections, device wiring | FiveDoMachine, Quartz | Core + chips |
FiveDo.Cheats | Per-game cheat parser, typed/conditional/button engine, atomic JSON store | CheatSet, CheatEntry, CheatDocument, CheatStore | Machine, Input |
FiveDo.Tas | TAS movie container, recorder/playback PBUS sources, branch/trim editor, frame/PC/state verifier | MovieFile, Movie, MovieHeader, MovieRecorder, MoviePbusSource, MovieEditor, MovieVerifier | Core, Input |
FiveDo.Host | Core thread/pacing, settings, BIOS/disc/NVRAM loading, audio/input backends, save slots, library, fixes, autopilot | CoreRunner, AudioFramePacer, AppSettings, AppSettingsStore, NvramStore, SaveStateSlotManager, StateInspector, GameLibraryScanner, GameCompatibility, MemoryPatch, RoadRashAutopilot | Core, Machine, Storage, Audio, Input, Video, Diagnostics |
FiveDo.Cli | Headless commands, JSON contracts | Program, CliJsonModels | Host |
FiveDo.Frontend.Avalonia | Desktop shell, library, settings, input, save slots, logs, donation banner, GPU probes | MainWindow, VideoSurface, SettingsWindow, InputBindingsWindow, SaveSlotsWindow, GameLibraryWindow, LogsWindow, WelcomeWindow, DonationEmailWindow, GlProbeWindow, GlProbeOptions, VkProbeRunner, VkProbeOptions, GlBackendOption, PlatformGamepadBackend | Host, Video.OpenGL, Video.Vulkan, Avalonia |
FiveDo.Tests | Unit/integration/golden/CLI/frontend tests | subsystem fixtures, ARM test assembler/rig, fake chips/MMIO, PNG writer | all production projects |
FiveDo.Benchmarks | Synthetic/real-content frame benchmark, gate, baseline | Program | Cpu, Madam, Dsp, Diagnostics, Machine, Storage, Video |
FiveDo.DiffHarness | 5DO vs OperaCore differential runs, manifests, allowlists, reports, baseline gate | Program, DiffManifestLoader, DiffComparer, OperaCoreHost, DiffReportWriter | Core, Video, Audio, Input, Storage |
4. Runtime composition¶
FiveDoMachine is the composition root:
var memory = new FiveDoMemory(tuning);
var clio = new Clio(tuning);
var sport = new Sport(memory.Ram, compatibility);
var vdlp = new Vdlp(memory.Ram, compatibility);
var madam = new Madam { Vdlp = vdlp, Interrupts = clio, Fifo = clio,
Bus = memory.Bus, Ram = memory.Ram, Compatibility = compatibility };
var xbus = new Xbus { Interrupts = clio };
var cdrom = new CdromDevice();
xbus.Attach(cdrom);
var dsp = new Dsp { Host = clio };
clio.Ram = memory.Ram;
clio.RomSelector = memory;
clio.Xbus = xbus;
clio.Dsp = dsp;
clio.Madam = madam;
memory.Madam = madam;
memory.Clio = clio;
memory.Sport = sport;
memory.DiagPort = diag;
var cpu = new Arm60(memory.Bus, clio, tuning, compatibility);
var quartz = new Quartz(tuning, () => clio.TimerDelay);
Wiring rules:
- Interrupts: every device raises FIQ through
IInterruptSink(CLIO). - CEL memory: MADAM gets the raw
Ramobject because the CEL engine uses rawmread/mwritesemantics that bypass the MMIO map. - VDLP memory:
Vdlpreads VRAM at the +2 MB offset, matching_mem_read32((addr & 0xFFFFF) + 2 MB). - SPORT memory:
Sportgets rawRamfor the VRAM pointer behavior. - XBUS: device 0 is the CD-ROM; up to 16 slots.
- DSP: CLIO owns the register windows; DSP raises audio FIQ through CLIO.
- Frame: VDLP writes
VdlFrame;FiveDoMachinecallsIFrameSink.OnFrameat V1. - Input:
FiveDoMachineasksIPbusSourcefor the PBUS packet at V1 (16-byte legacy joypad layout or the Opera device chain, up to the 256-byte MADAM PBUS buffer).
5. Frame loop¶
FiveDoMachine.RunFrame mirrors _3do_Frame:
Clio.OnFrame()
total = 0; batch = 0; frameCycles = ArmClock / 60
while total < frameCycles:
if Madam.Fsm == InProcess:
Madam.HandleCel(); Madam.Fsm = Idle; continue
if instructionObserver != null:
observer(PC, CPSR, Bus.Read32(PC))
batch += Cpu.Step()
if batch < 16: continue
InternalFrame(batch, skipFrame)
total += batch; batch = 0
framesExecuted++; lastFrameCycles = total
InternalFrame mirrors _3do_InternalFrame:
Quartz.PushArmCycles(cycles).- If
QueueDsp():Dsp.Loop(), incrementAudioSamplesProduced, push to the sink. - If
QueueTimer():Clio.DoTimers(). - If
!QueueVdl(): return. line = Quartz.VdCurrLine;Clio.UpdateVcnt(line, halfFrame).- If not
skipFrame:Vdlp.DoLine(line, frame). - If
line == 16 && skipFrame:FrameSink.OnFrameTrigger(). - If
line == Clio.V0Line: FIQ reason1 << 0. - If
line != Clio.V1Line: return. - FIQ reason
1 << 1; copy PBUS input;Madam.KeyPressed. - Set
SourceWidth/SourceHeight(640x480 in high-res), widen to the widest line. - If not
skipFrame:FrameSink.OnFrame(frame).
Invariants:
- The reference processes at most one DSP/timer/VDL event per
InternalFramecall. Do not drain all due events in a loop without golden-frame evidence. - The 16-cycle batching threshold (
batch < 16) is reference behavior. Quartz.PushArmCyclesexpects batches < 256 cycles;clks << 24would overflow.- The DSP must run even when no audio sink is attached; the guest uses it for allocation/semaphore handshakes.
- The VDL frame is reused; the sink must copy/upload before returning.
RunFrames(n) simply calls RunFrame() n times. RenderFrameMultitask() renders 256 lines from the current VDLP state for the optional multitask path.
6. Subsystem invariants¶
Core contracts¶
IAddressBuspreserves FreeDOCore's decode order and word-swapped byte convention.IRamis raw RAM/VRAM access; offsets are byte offsets into the allocation, not CPU addresses.IStateSectionwrites explicit fields; never serialize object layouts.VdlFrame/VdlLineare reusable; copy before cross-thread handoff.AudioSamplepacks left in bits 0..15 and right in bits 16..31.FixModebits are persisted; never renumber them.
CPU¶
- One instruction per
Step(), with the reference's cycle count. - Preserve decode order, C++ switch fallthrough, banked registers, the CLIO PC-adjust quirk, Anvil/Crash 'n Burn hacks, and immediate data-processing carry-out.
- All mutable state is instance state; independent machines can run in parallel.
- The interpreter is the compatibility oracle; a future JIT must be bit-identical.
Memory¶
- RAM/ROM are
uint[]words whose values match a little-endian C++ build after_bswap/memcpy. - Byte accesses use lane
addr & 3; halfword accesses useaddr & 3; CPU byte/halfword accesses applyaddr ^ 3/addr ^ 2. - NVRAM is a
byte[32 * 1024]because FreeDO maps one byte per 32-bit word. - The RAM backing includes 16 reserved 1 MB high-res mirrors; only 3 MB are CPU-visible.
- ROM loading converts big-endian BIOS words with
ReadUInt32BigEndian.
MADAM/CEL¶
Madamowns 2048+64 registers, PLUT, PBUS queue, CEL FSM, matrix results.CelEnginewalks CCBs;CelRasterizerdraws packed/literal/LR/arbitrary CELs.CelMemoryaccounts +1/+2/+4 CEL cycle costs.CelRasterizer.PreparePipelinecaches the CCB's pixel source/target, modulos, PLUT, flags, and control words once per CEL. It must run afterPrepareGeometryand before any pixel write; batched fast paths that bypassCelMemorymust add the same read/write cycle counts explicitly.- High-res
ResScaleaddresses four 1 MB VRAM planes; preserve plane 0/1 vs 2/3 semantics. - Fix-mode quirks (
BitTiming3/6,GraphicsStepY, speedfixes, unknownflag11) are applied in the CEL path.
CLIO¶
- Registers/timers/FIFOs/DMA/interrupts;
DoTimersandUpdateVcntdrive the frame. - XBUS/DSP windows are routed through CLIO.
- ROM bank select is register
0x84. - The PC-adjust pending flag is consumed by the CPU/memory.
DSP¶
- 16-bit fixed-point core with NMem (2048) and IMem (1024), semaphore, multiplier, ALU, barrel shifter.
Loop()executes one DSP instruction/sample event; precomputed tables are rebuilt on load.- Semaphore handshakes must work without an audio sink.
- Sample output is packed left/right; the host clock is exact 44.1 kHz.
SPORT¶
- Fill/copy engine with color/source/destination registers.
- In high-res mode, copies mirror across all four 1 MB planes.
VDLP/Video¶
- Parses display lists at
(addr & 0xFFFFF) + 2 MB. - Line widths 320/384/512/640/1024;
VdlLine.MaxPixels = 1280. - High-res line-doubles 640x240 to 640x480 via planes.
- The CPU
FrameAssembleris the oracle; the GPUAccuratemode must match.
XBUS/CD-ROM¶
Xbusowns up to 16IXbusDeviceslots; device 0 is the CD.CdromDeviceimplements MEI commands, poll/status/data, TOC/MSF/LBA, and the READ_ID media-access pulse (two polls >= 50 frames apart set poll bit0x40).- CD timing is approximate; do not port
opera_cdrom.cwholesale without evidence.
Storage¶
IDiscSourcenormalizes every container to 2048-byte sectors.- ISO direct; BIN 2352 with payload at 16/24; CUE first data track; CHD CD via CHDSharp.
GameIdis MD5 of the first two sectors.BiosImagerequires exactly 1 MB BIOS1; BIOS2 optional <=1 MB.NvramImagefresh header matches 4DO.PrefetchingDiscSourceis opt-in and bounded.
Audio¶
- Exact 44.1 kHz DSP clock; packed sample format.
- Ring buffer is SPSC, power-of-two, drop-on-full.
- OpenAL fill loop rents released buffers only; variable-size blocks; 8 buffers; 3072-frame prefill; soft limiter 0.8/0.9.
IAudioSink.Resetdrops old audio on reset/state load.- WAV capture is raw DSP output; tap records exact queued PCM.
Input¶
ControllerButtonflags map to the two PBUS bytes.- Legacy PBUS order is 1, 3, 2, 5, 4, 6; active high byte has bit 7 set. Ports 7-8 and mixed peripherals use the Opera daisy chain.
InputScriptframes are absolute;--script-base stateoffsets.InputRecordercoalesces unchanged player-1 states into the same script format;InputManager.Recorderis called on the emulation thread andFrameProvidersupplies the frame number. The desktop writes<game>-<timestamp>-input.txt.- Keyboard/gamepad profiles are per-player; gamepad fallbacks for sticks/triggers.
HotkeyRouterresolves the persisted console hotkeys; the shell dispatches them before forwarding a key to the emulated pad. F11 works from the library, and Escape leaves fullscreen before the pause action runs. F6/F7 change slots; F9 steps one frame throughCoreRunner.StepFrameand leaves emulation paused.
Diagnostics¶
Loglevels/categories; no timestamps in core logs.CpuTracer,FrameTiming,PngDumper,FrameDumper.BenchmarkGate: 0 bytes/frame, core p99 <= 8 ms, total mean <= 8 ms, total p99 <= 16.667 ms.BenchmarkComparison: same-machine, default 10% regression, exit 4.
Machine/Quartz¶
FiveDoMachineowns reset order, frame loop, state sections, device wiring.Quartzuses 8.24 fixed point for ARM/VDL/timer and an exact rational DSP clock.- Reset order: CPU, memory, VDLP, SPORT, MADAM, XBUS, CLIO, DSP, Diag, Quartz; audio sink reset.
- State load restores sections and calls
AudioSink.Reset.
Host¶
CoreRunnerowns the core thread; all machine access goes throughInvoke.AudioFramePacerderives the period from samples; 60 Hz fallback; 50-70 clamp.AppSettings/AppSettingsStoreare source-generated JSON, atomic writes.NvramStore250 ms debounce, atomic writes,.badon corruption.SaveStateSlotManagerslots/metadata/thumbnails; state header authoritative.GameLibraryScannerextensions/depth/cover art.GameCompatibilitymaps 4DO IDs to fix bits.RoadRashAutopilotis a test harness; do not generalize it without evidence.
CLI¶
Programdispatches commands;ParsedOptionsparses--name value/flags.- JSON contracts are source-generated in
CliJsonModels.cs. - Most JSON is camelCase;
state inspect/state slotsare PascalCase. - Exit codes 0/1/2; benchmark 3/4.
- Diagnostics write text files;
--frame-outPNG/BMP;--dump-dsplittle-endian words.
Frontend¶
MainWindowowns the shell, session lifecycle, settings, library, logs, screenshots.VideoSurfacehosts the GL context andOpenGlRenderer.CpuVideoRenderer+WriteableBitmapfallback.SettingsWindow,InputBindingsWindow,SaveSlotsWindow,GameLibraryWindow,LogsWindow,WelcomeWindow.PlatformGamepadBackendselects XInput (Windows) or evdev (Linux); macOS has no backend.GlProbeWindow/GlProbeOptionsimplement the GPU probe.- Compiled bindings, no reflection-based
ViewLocator.
Tests/benchmarks¶
- One xUnit project with subsystem folders; opt-in real BIOS/GPU/audio tests.
TestUtilitieshas an ARM assembler/rig, fake chips/MMIO, PNG writer.FiveDo.Benchmarksis a stopwatch harness, not BenchmarkDotNet; it measures core, pack, mailbox, read copy, allocations, p50/p95/p99, gate, baseline.
7. Threading model¶
| Thread | Owns | Rules |
|---|---|---|
| Core thread | CPU, memory, chips, scheduler, DSP, VDLP, CD | Single-threaded, deterministic; CoreRunner.Run calls RunFrame; UI actions use Invoke. |
| Render/UI thread | Avalonia window, GL context, presenter | Consumes VdlFrameMailbox; never calls RunFrame; renders on demand. |
| Audio fill thread | OpenAL source/buffers | Consumes the ring; never calls into the core; makes the context current on its own thread. |
| Host timers | status/log refresh | Low rate (4-10 Hz); no per-frame formatting. |
Cross-thread state: audio ring, latest-frame mailbox, session lifecycle flags. No locks in the core hot path. CoreRunner.Invoke pauses the loop, runs the action, and resumes. VdlFrameMailbox.Read copies under a short lock and invokes the renderer outside it.
Pacing: AudioFramePacer (30-frame average, 50-70 fps clamp, 60 Hz fallback) sets FramePacer's period. FramePacer sleeps for the bulk of the wait and spins for the last millisecond; a >100 ms stall resyncs rather than chasing the past.
8. State ownership¶
| State | Owner | In .tdos? |
|---|---|---|
| CPU registers/CPSR/banked registers/FIQ/ROM select/data abort | Arm60 | yes |
| Full RAM backing (largest layout: 14 MB DRAM + four 2 MB VRAM planes) | FiveDoMemory | yes |
| ROM banks | FiveDoMemory | yes |
| NVRAM | FiveDoMemory | yes |
| MADAM registers/PLUT/PBUS/CEL FSM/matrix temps | Madam | yes |
| CLIO registers/timers/FIFOs/DMA | Clio | yes |
| DSP registers/NMem/IMem/semaphore/seed | Dsp | yes |
| SPORT color/source/destination | Sport | yes |
| VDLP head/current bitmap/palette/line delay | Vdlp | yes |
| XBUS select/poll/FIFOs/attached device state | Xbus | yes |
| CD command/status/data/TOC | CdromDevice | yes (also via XBUS) |
| Quartz accumulators/line counter | Quartz | yes (v2) |
| Compatibility flags + VDL frame | FiveDoMachine | yes (Machine section) |
| Disc contents | host IDiscSource | no (hash recorded, not validated) |
| Input state | host | no |
| Audio queue | host audio backend | no |
| Settings/save metadata | host | no |
9. Error handling and logging¶
- Fatal configuration errors (missing/invalid BIOS, unsupported disc, invalid state) throw host/storage exceptions (
InvalidDataException,DiscImageException,InvalidStateException,ArgumentException). The CLI catches these and exits 1. - Non-fatal emulation anomalies (unmapped MMIO, unknown CD command, malformed CEL) log at Warning/Debug and continue with the reference sentinel behavior (
0xBADACCE5, zero, etc.). - No exceptions in the hot path. Device methods return values; diagnostics use Serilog level guards.
- Logging: Serilog with levels/categories.
LoggingSetupconfigures rolling files, the in-memory viewer, stderr (desktop), and the bug-report sink. The desktop usesInMemorySerilogSink(4000 entries). The CLI keeps stdout/stderr for its JSON/product contract and writes diagnostics to the file/API. - State warnings:
FiveDoMachine.StateWarningsreports version/BIOS/unknown-section warnings; the CLI includes them inrun --jsonand the UI logs them. - Core faults:
CoreRunner.Faultedreports the exception; the thread stops and logs through Serilog.
10. Extension points¶
| Extension | Interface | Existing examples |
|---|---|---|
| Video presenter | IFrameSink, IVideoRenderer | OpenGL/GLES, CPU WriteableBitmap, null/hash sink |
| Audio output | IAudioSink | null, WAV, OpenAL |
| Input backend | IPbusSource, IGamepadBackend | keyboard, scripted, XInput, evdev |
| Disc format | IDiscSource | ISO, BIN, CUE, CHD, prefetch wrapper |
| Save state | IStateReader/IStateWriter, IStateSection | native .tdos sections |
| CPU backend | ICpuBackend | ARM60 interpreter; optional IL JIT (not implemented) |
| XBUS device | IXbusDevice | CD-ROM; future peripherals |
| Renderer/filter | IVideoRenderer, shader variants | nearest, sharp-bilinear, CRT, xBR, HQx, FSR |
| Logging | Serilog.ILogger, ILogEventSink | file, stderr, in-memory viewer, bug-report API |
| Benchmark policy | BenchmarkGate, BenchmarkComparison | CI gate, same-machine baseline |
Adding a renderer filter¶
- Add the enum value in
RendererContracts.cs. - Implement the pass in
OpenGlRendererand shaders inShaders.cs(GLSL 330 + ES 300 variants). - Add settings/validation if it has parameters.
- Add tests: shader source, settings, parity/cost if applicable.
- Add UI enum (the combo binds
Enum.GetValues<VideoFilter>()) and docs. - Measure at 4K; keep it opt-in if expensive.
Adding a disc format¶
- Implement
IDiscSource(2048-byte sectors,SectorCount,ReadSector). - Add detection/opening in
DiscImageFactory. - Add tests for geometry, payload offset, errors.
- Update
GameLibraryScannerextension list and docs. - Keep the CD device container-agnostic.
Adding a state section¶
- Choose a stable ID in
StateSectionIds(never renumber). - Implement
IStateSectionon the owner. - Write/read explicit fields; bump the section version if the layout changes.
- Add it to
FiveDoMachine.SaveStateand the required set/switch inLoadState. - Add roundtrip/truncation/CRC tests and docs.
Adding a CLI command¶
- Add the dispatch in
Program.Main. - Parse with
ParsedOptions; add a result record +[JsonSerializable]inCliJsonModels.cs. - Add
--jsonand text output; keep exit codes consistent. - Add process/JSON tests.
- Document in
CliReference.md.
11. Coding conventions¶
- File-scoped namespaces; one public top-level type per file.
sealedunless inheritance is required.readonlyfields where possible; no mutable static state (constants/tables are fine).uint/ushort/bytefor emulated registers;intonly where the reference uses signed arithmetic.- Explicit
uncheckedaround intentional wraparound. - Constants for masks/register offsets; no bare magic numbers in new code.
- XML documentation on public types; ported code includes
// FreeDO: <file>:<function>. - Tests named
Method_Scenario_ExpectedResult. - No
unsafeunless a measured hot path requires it;Unsafe/MemoryMarshalare acceptable. - No LINQ, string formatting, delegate allocation, or managed allocation in per-instruction/per-frame paths.
- Prefer concrete types/generic constraints over interface dispatch in hot loops; keep Core interfaces at architectural boundaries.
- Shaders live in
FiveDo.Video.OpenGL/Shaders.cs, have GLSL 330/ES 300 variants, and are embedded/tested. - JSON uses source-generated contexts; do not add reflection-based
JsonSerializeroverloads to trimmed paths. - Keep the trimmed/AOT paths healthy: no reflection, no dynamic assembly loading, no
TypeDescriptoron first-party paths.
12. How to make a change¶
- Reproduce and reference. Identify the FreeDO/Opera behavior and the expected output. Add a failing test or record a real-content hash.
- Choose the layer. Core contract if cross-chip; chip project if internal; host if OS/wall-clock; frontend if UI.
- Keep determinism. No wall clock, no random, no hidden globals, no thread in the core.
- Add state. If the feature is stateful, add a section/fields and roundtrip tests.
- Add diagnostics. A trace/log/CLI flag is cheaper than guessing.
- Test. Unit + integration + golden/differential where applicable; opt-in real-content/GPU/audio tests for hardware paths.
- Measure. Run the benchmark/gate; for presentation, run the GPU probe.
- Format.
dotnet format 5DO.sln --verify-no-changes(or apply). - Document. Update the user/reference/developer docs and
CHANGELOG.md. - Validate trimmed/AOT. Publish trimmed and run the smoke tests if you touched JSON, reflection, or startup.
Definition of done for a ported subsystem:
- Reference behavior documented.
- Unit tests for edge cases.
- Golden/differential evidence where content allows.
- Save/load roundtrip if stateful.
- No steady-state allocations.
- Diagnostics available.
- Docs updated.
13. Debugging entry points¶
| Entry | Use |
|---|---|
FiveDoMachine.FrameSink | Capture/replace frame output. |
FiveDoMachine.PbusSource | Script input. |
FiveDoMachine.AudioSink | WAV/OpenAL/custom sink. |
FiveDoMachine.StateWarnings | Inspect load warnings. |
FiveDoMachine.Memory.RamWriteObserver | RAM watchpoints. |
FiveDoMachine.Clio.TraceWriter / FiqObserver | CLIO/FIQ traces. |
FiveDoMachine.Cdrom.TraceWriter / CommandObserver | CD traces/stop conditions. |
FiveDoMachine.Madam.KPrint / CelTraceWriter | KPRINT/CEL traces. |
FiveDoMachine.Dsp.DspTraceEnabled / DspTrace | DSP instruction ring. |
CoreRunner.Faulted | Core thread exceptions. |
CoreRunner.Invoke | Safe machine access from UI. |
VdlFrameMailbox | Frame handoff. |
StateInspector | Header/section inspection. |
BenchmarkGate/BenchmarkComparison | Performance policy. |
GlProbeWindow | Real GL path measurement/parity. |
CLI equivalents are in Diagnostics.md.
14. Reference mapping¶
| FreeDO/4DO source | C# counterpart |
|---|---|
arm.cpp | FiveDo.Cpu/Arm60, Arm60State, ArmDecoder, ArmAlu, ArmBarrelShifter |
_3do_sys.cpp, frame.cpp | FiveDo.Machine/FiveDoMachine, FiveDo.Video/FrameAssembler |
Clio.cpp | FiveDo.Clio/Clio, DiagPort |
Madam.cpp, CEL sources | FiveDo.Madam/Madam, CelEngine, CelRasterizer, Pixel*, Projector |
DSP.cpp | FiveDo.Dsp/Dsp, DspInstruction |
SPORT.cpp | FiveDo.Sport/Sport |
vdlp.cpp | FiveDo.Video/Vdlp |
XBUS.cpp, Iso.cpp | FiveDo.Xbus/Xbus, FiveDo.Cdrom/CdromDevice, FiveDo.Storage |
quarz.cpp | FiveDo.Machine/Quartz |
DiagPort.cpp | FiveDo.Clio/DiagPort |
Filters/ | FiveDo.Video.OpenGL shaders/filters |
Worker.cpp | FiveDo.Host/CoreRunner, AudioFramePacer |
GameDatabase.xml | FiveDo.Storage/GameDatabase (embedded resource) |
| Opera/libopera | Behavioral reference, not a source port; see ReferenceStrategy.md |
15. Known architectural debt¶
These are real, current limitations. Do not "fix" them casually without tests.
- Word-swapped memory representation. RAM/ROM words match a little-endian C++ build; byte/halfword accesses apply lane XORs. A clean big-endian refactor would touch hundreds of places and must be guarded by the full suite/state compatibility.
- Large save states. The Memory section writes the entire 22 MB RAM backing, including high-res mirrors; the Machine section writes all 960 VDL lines. Format version 2 compresses large sections with Brotli (typically 0.6-2.0 MB), but the in-memory state and the uncompressed payloads remain ~25 MB. Version-1 states are still readable. Further reductions (trimming duplicate mirrors) are a format change.
- Disc hash is warned, not enforced. Loading a state from a different disc adds a
StateWarningsentry but still loads; the CLI and desktop surface the warning. RendererandOutputResolutionare honored at startup and when Settings is applied. The shell can use OpenGL, Vulkan, or the CPU renderer; changing the backend recreates the active presenter without restarting emulation. Fixed output resolutions render offscreen and scale into the window. GL init failure or context loss still falls back automatically.fshas its own Opera parser.FiveDo.Storage.OperaFileSystemreads the volume/directory records directly from anyIDiscSource, so ISO, BIN, CUE+BIN, and CHD share one code path.disc dump,info, anddisc tracksremain the raw-container inspection tools.- CLI JSON casing is split.
state inspect/state slotsare PascalCase for historical compatibility; other commands are camelCase. - Some stale code comments. For example,
EmulatorTuning.HighResolutionsays the mode is reserved, but it is implemented;Vdlpsays high-res is left for M11, but it is implemented. Treat behavior/tests as authoritative. - Frontend NativeAOT audio is validated with OpenAL's null device. The frontend AOT build loads the bundled OpenAL Soft runtime and reports
OpenAL audio readywithALSOFT_DRIVERS=null; a physical-device listen remains a manual check. - No guest JIT. The ARM60 interpreter is the only backend;
ICpuBackendexists for a future managed IL JIT. AudioPipelineis not the live path. It is a reusable host ring sink; the desktop/CLI live path usesOpenAlAudioOutputandWavAudioSinkdirectly.- Custom aspect is edited as a numeric ratio.
RendererSettings.CustomAspectexists and the Settings dialog exposes a positive width/height field; presets are still the common 4:3/16:9 modes. - CD timing is approximate. The MEI state machine/poll behavior is a reference port, not a cycle-accurate drive model. Do not port
opera_cdrom.cwholesale. - Platform validation gaps. Windows x64 is validated; Linux/macOS launch, arm64, GLES 3.1, Tier B desktop timing, and DPI are incomplete. Raspberry Pi is out of scope.
- Old docs drift. If a doc disagrees with code, fix the doc in the same change; this guide and the documentation index are the entry points.
16. Related documents¶
Architecture.md- design goals, composition, pipelines.BuildAndTest.md- build/test/publish/CI.TestStrategy.md- test layers and evidence.PortingNotes.md- C++ to C# rules and quirks.ReferenceStrategy.md- Opera/FreeDO hierarchy.Audio.md,Video.md- subsystem deep dives.SaveStates.md- state format.Diagnostics.md- traces/probes.Troubleshooting.md- symptom-driven fixes.Trimming.md- trim/AOT constraints.Performance.md- budgets and measurements.Compatibility.md,KnownIssues.md- current behavior.