Skip to content

Instantly share code, notes, and snippets.

@yupferris
Created January 23, 2017 02:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yupferris/b476763839094998c91ddb9ecc758984 to your computer and use it in GitHub Desktop.
Save yupferris/b476763839094998c91ddb9ecc758984 to your computer and use it in GitHub Desktop.
Steps to a more decoupled emulator

Generally, the emulator core should be more or less a black box, where you tell it to step and give it some sinks for audio/video, and it tells you how many clock cycles passed while stepping and it may or may not dump some data into those sinks. For the most part, the VirtualBoy struct already has this interface, along with AudioDriver and VideoDriver as sinks. However, it's a bit crap right now as the emu also relies on audio for a timing source, which means we can't really have a configuration that doesn't use that atm.

The reason we want to use audio as a timing source is because audio cards tend not to have timers that are perfectly synced to the computer's timer, esp. at weird sample rates. As a consequence, there will be periodic clicks and pops as the audio output buffers and the emulator go slightly out of sync. Relying on audio output as a timing source fixes this, but couples us to audio output.

It would be nice to have some kinds of abstractions; for example, a TimingSource abstraction, that could be implemented by the audio output unit or a timer for the case where we don't have audio. However, I suspect either one or both of the audio unit interface and the timer interface would need mutable references to work, which means we couldn't actually have this implemented by the same object in Rust, as we'd run into ownership problems. We could get around this by saying all audio sinks are always time sources, but in cases like a wave output sink or just a null sink we'd also have to implement dummy timers, which isn't really ideal and doesn't feel like the right coupling.

Ideally, having a separate, high-resolution timer driving the emulation core and expecting the audio output to be in sync is our best choice decoupling-wise. If the timer is high-resolution enough and there's not any noticeable drift, it may actually work out that there aren't the clicks/pops I mentioned earlier. Given this would provide the best architecture for this project moving forward (and in the longer-term, the emu project, which would house the traits for this architecture as well as a bunch of interchangeable structs implementing those traits), I think it's actually worth trying. If there ends up being consistent timing drift, we can always as a backup couple audio output and timing again, just with a more clear interface.

So, some concrete steps for plan A (the "decouple all the things" plan):

  • Rename audio_driver -> audio_sink and video_driver -> video_sink to clarify what these are for
  • Introduce another audio concept with append_buffer fn
    • append_buffer is distinct from audio_sink in that audio_sink is a per-frame sync, whereas this audio concept would take in buffers. This is to minimize cross-thread communication, which most (if not all) audio backends require to some degree.
  • Implement this audio concept for CPAL (really just refactor CpalEngine for now)
    • This interface/implementation should hide away all the threading stuff so we won't have another deadlock situation :)
  • Introduce timer and top-level cycle tracker for emulator
    • This way we can keep track of both real time and emulation time and make sure the emulator is always kept running appropriately. This should basically be the same as what happens now with the audio stuff and desired_frames, but with timestamps rather than amount of audio frames processed.
  • Introduce additional audio implementations, such as .wav dump and null-output
    • These will be trivial sinks in this case

If all of that works, great! However, it could very well have some drift, so a plan B would also help:

  • Rename audio_driver -> audio_sink and video_driver -> video_sink to clarify what these are for
    • Same as above
  • Introduce another audio concept with append_buffer fn and get_time fn
    • Same as above but with get_time in addition
  • Implement this audio concept for CPAL and adjust top-level code accordingly
  • Introduce additional audio implementations
    • Same as above but these will also include timers.

After all that we should be good to go without audio, and should also have a really nicely decoupled architecture around a nicely-isolated emulation core! Things to do after this would include better config for swapping out these impl's dynamically (or at least at startup with cmd line args), starting to isolate the debugger, and perhaps trying to decouple video/input a bit more, but I think those are fine for now as it's less likely those will cause problems. All of these things will be part of a contributing list I want to make, so I can invite people to contribute and help out. There are a lot of small things to make an emulator usable; it's dangerous to go alone :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment