Search/Build an Emulator That Matches Real Hardware Bus and Timing Behavior

Build an Emulator That Matches Real Hardware Bus and Timing Behavior

Use when building or fixing an emulator. It splits code by hardware part, copies bus reads, writes, timing, and DMA exactly, tests with programs known to pass on real hardware, and keeps save states and the public API stable.

To use this AI rule
## Explain Unusual Hardware Code and Run Type Checks and Tests Write emulator code for engineers who may not know the original hardware. Explain why a timing rule, side effect, or workaround exists instead of assuming that an unusual implementation is self-evident. Link authoritative hardware references where they clarify behavior. Discover and use the repository's documented commands for formatting, linting, type checking, tests, builds, watch mode, and benchmarks. Treat required checks as part of the change: - Run formatting and the focused tests while iterating. - Run type checks when public declarations or typed boundaries change. - Run the authoritative full suite before committing or handing off. - Run the performance benchmark for major changes or suspected regressions. - Keep unrelated refactoring separate from a focused fix. Update public examples and API references when methods, options, callbacks, exports, or supported input forms change. Put local implementation details in nearby comments; reserve project-level guidance for architecture and rules that apply across several components. ## Split the Emulator into Components That Match the Hardware Parts Organize the emulator around the hardware subsystems it models. Use a small orchestrator to coordinate components and expose callbacks for rendering, audio, status, and persistent-memory updates. Keep processors, video, audio, cartridge or memory mapping, program loading, rendering helpers, controllers, and serialization responsibilities separate. Give each component a clear interface and let it own its internal state. Use inheritance only when hardware variants share a genuine base behavior and override specific mapping or banking rules. Prefer composition for independent subsystems and avoid hidden access across component boundaries. Define a common time base. Frame-based execution still requires precise cycle accounting inside each frame, while host applications remain responsible for presentation timing and scheduling. Centralize save-state conversion through symmetric serialization and restoration contracts used by every stateful component. ## Reproduce Every Bus Read and Write, Including Dummy Accesses Model the bus as stateful behavior, not as a pure address-to-value lookup. Real hardware may retain the most recent value, return latched data from unmapped or write-only addresses, or drive only selected bits. Track separate latches when different hardware buses retain independent state. Update the appropriate latch on every observable load, write, stack operation, opcode fetch, and interrupt-vector fetch. When a device drives only part of a value, combine those bits with the retained bus state exactly as the hardware does. Implement dummy reads and writes as real bus operations. They can update latched values and trigger I/O side effects even when the program never consumes the returned value. Encode the bus sequence for each addressing mode explicitly: - Indexed addressing may read an intermediate address before a page correction. - Zero-page indexing may read the unindexed address before applying the index. - Indirect indexing may read a pointer or intermediate address before reaching the effective address. - Stores and read-modify-write instructions may perform mandatory dummy accesses even without a page crossing. - A read-modify-write instruction may write the original value before writing the modified value. Preserve instruction-specific ordering when a fetch occurs before or after stack activity. Reordering operations that produce the same final register values can still be observably incorrect on the bus. ## Keep Devices in Step Mid-Instruction and Treat DMA as Real Bus Cycles When the emulator executes one component atomically but real hardware advances components in lockstep, stale state can appear at mid-instruction register accesses. Correct this at the boundary where the access becomes observable. Track how many bus operations the current instruction has completed. Before a synchronized device register read or write, advance that device by the exact clock ratio for the elapsed operations. Subtract already-applied work from the later frame-level advancement so time is never counted twice. Model interrupt flag creation separately from interrupt delivery. An inhibit control may prevent a status flag from being created, rather than merely suppressing the external interrupt signal. Tests must distinguish these behaviors. Represent DMA as ownership of real bus cycles. A transfer may alter the current bus value, delay processor progress, or change the result of instructions whose output depends on bus state. Preserve the ordering between DMA, instruction accesses, and device side effects. Implement documented unofficial or edge-case instructions only from reliable references and tests. Do not simplify their cycle behavior merely because common software does not use it. ## Test with Programs Known to Pass on Real Hardware Use several layers of tests, each with a distinct purpose: - Unit tests verify initialization, instruction semantics, mapping rules, memory mirroring, state transitions, and error handling. - Focused regression tests lock down frame buffers, audio samples, bus traces, or other deterministic outputs. - Integration harnesses run trusted diagnostic programs and compare detailed machine state or error codes. - Hardware-accuracy suites cover interactions that isolated instruction tests cannot expose. Treat established reference programs as correct when they are known to pass on real hardware. Investigate the emulator, harness, fixture, and environment before blaming the reference program. Record accepted failures explicitly with a narrow reason. A known-failure list is not permission to hide new regressions or broaden an existing exception. Test both final values and operation order when hardware can observe intermediate accesses. Add a regression case whenever a specification correction or previously misclassified behavior changes the implementation. ## Make Save States Fully Restorable and Stop Running After a Crash Save states must include every component that affects future execution, including processor, video, audio, mapper, latch, interrupt, and timing state. Verify that serialization followed by restoration reproduces execution, not merely that the data can be parsed. Expose persistent memory through a clear callback or storage boundary rather than coupling the core to one host environment. Keep ordinary controllers as simple state machines with explicit button transitions and serial protocols. Model specialized peripherals through dedicated APIs that preserve their distinct spatial, timing, or light-detection behavior. After an unrecoverable execution exception, set an explicit crashed state and prevent further frame execution. Require a documented reset or program reload to recover instead of continuing from partially mutated state. Keep accepted program inputs and public callback types stable. Maintain public type declarations alongside the implementation so browser and server consumers observe the same API contract. ## Create Generated Files Only Through the Build and Report What Stayed Untested Generate distribution bundles, minified files, source maps, and declarations through the approved build. Do not edit generated outputs directly. Verify every supported runtime target and module format that the project claims. Continuous integration should run the same formatting, linting, build, type, and test gates required locally. Before changing hardware behavior, consult authoritative specifications and trusted implementation notes. Document the reason for non-obvious code near the affected operation, including the observable consequence of changing its order. Review the final diff for accidental generated-file edits, stale public documentation, untested timing paths, and unrelated cleanup. Report the focused checks that ran, benchmark results when relevant, known failures, and any hardware path that remains unverified.