1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-30 04:30:02 +00:00

Initial draft.

Thomas Harte 2019-03-07 12:03:55 -05:00
parent ee95b600ea
commit 70524e4daf

132
Porting-Guide.md Normal file

@ -0,0 +1,132 @@
# Porting Clock Signal to New Platforms
Disclaimer: this guide discusses various internal implementation details, which means that it contains information that might change. I will attempt to include commentary on how stable all discussed mechanisms have empirically proven to be, and what future plans currently are.
## What a New Port Must Do
Machines provide a common interface for connection to the outside world.
A new port should:
* have some means to collect input and forward it to an emulated machine;
* provide somewhere to which an emulated machine can forward audio and video output; and
* provide time updates to the emulated machine.
_At present only keyboard and analogue and digital joystick input is defined; in the future I intended to support mouse-type devices, paddles, etc._
### Suggested Point of Entry for Code
See [https://github.com/TomHarte/CLK/blob/master/OSBindings/SDL/main.cpp|the SDL target]. That single file is the entirety of it. No further SDL-specific code exists in the repository and no additional changes were required for the SDL port to operate.
## Obtaining a Machine
### ... based on a piece of media
Step one: run the static analyser on the media. Call `Analyser::Static::GetTargets(<file name>)`. It'll return a list of all the machine configurations that might be correct for that piece of media. Most of the time it'll be exactly one, but it may be more where static analysis didn't prove sufficient.
If no potential targets are returned, the emulator does not know how to provide a machine for that piece of media.
Step two: construct a machine to run those targets by calling `Machine::MachineForTargets(targets, rom_fetcher, error)`. A single machine contains all potential targets. Only the one believed most likely to be correct will be visible, with dynamic analysis calculating that on a rolling basis.
The `rom_fetcher` is a lambda that accepts: (i) the name of a machine (e.g. 'Electron'); and (ii) a list of required ROM file names for that machine (e.g. 'os100.rom'). It should return the contents of those ROMs where they can be found. It is up to the specific target to decide how to obtain those ROM contents. The Mac version checks the app bundle's resources folder, the SDL version checks some well-defined global paths if no command-line option was present, hypothetical other versions might obtain the ROMs from the internet or provide versions built into the executable, or take any other step they require.
`error` will be filled in with any error that was encountered in creating the machine.
### ... without a piece of media
_At present there is no reflective way to achieve this; this is considered a significant flaw and is why this feature is currently available on the Mac only. Hesitation has arisen because it intersects with a bigger question about maintainably providing reflective data structures in C++, which would pair off with any future work on debugging capabilities and possibly consume the current means for providing runtime options (see below)._
In `Analyser/Static/StaticAnalyser.hpp` you'll see the definition for `Analyser::Static::Target`, which is the base class for the targets you can supply to `Machine::MachineForTargets`. In practice most machines define a custom subclass of this that adds the machine specific static configuration (e.g. how much RAM, which region — things which a real machine can't change at runtime). E.g. `Analyser/Static/MSX/Target.hpp` defines an MSX target to be one that adds: (i) a boolean to indicate whether a disk drive is present; (ii) a string 'loading command'; and (iii) a region.
So, simply instantiate an appropriate `Target`, and supply it in a single-item vector to `Machine::MachineForTargets`.
(Aside: the loading command pattern is reasonably common but not universal. It's a string that, if specified, the machine will automatically type after booting up. It's nonsensical for machines like the Atari 2600, ColecoVision and Master System, but useful for machines like the Electron (when loading from tape) or the Vic-20 where launching a particular piece of media requires a command to be executed, sometimes specific to that particular instance of that type of media. So it is by this field that the static analyser can provide the command to be loaded.)
## 'Dynamic' Machines
A dynamic machine is one that provides a discoverable interface; the `DynamicMachine` returned by `Machine::MachineForTargets` provides a mechanism for answering questions such as 'Does this machine have a keyboard?', 'Does it have any status LEDs?', 'Does it produce sound?' through runtime discovery. It is specifically equivalent to `dynamic_cast`.
See `Machines/DynamicMachine.hpp` for the different types of interface a dynamic machine may vend. They're discussed in more detail below.
## Aside on: 'CRT Machines'
Historically, the common denominator of all implemented machines was that they used a CRT for output, and therefore provided an instance of the internal CRT emulation. From there comes the idea of a `CRTMachine`. *The name became outdated in March 2019, and this interface is likely to change soon*.
## Providing Time
Emulated machines are advanced via the `CRTMachine` interface via `run_for(Time::Seconds)`. `Time::Seconds` is a floating-point type.
This interface is specified in real time rather than e.g. clock cycles because if dynamic analysis is ongoing then it might involve machines of different clock rates.
## Providing Keyboard Input
Not all machines have a keyboard; a machine holder must therefore check that the `KeyboardMachine::Machine` vended by a dynamic machine is non-null.
Each keyboard machine also provides both:
* a default mapping from PC-style keyboards to that machine; and
* a machine-specific set of keys.
If you don't want to write specific machine-by-machine code, use the former. Use `get_keyboard` on the machine to get an instance of `Inputs::Keyboard`. See its `enum` for the list of keys that approximates a PC layout. Use `set_key_pressed` for announcing key up and key down, and `reset_all_keys` if useful (in practice, it can be an easier way to deal with task switching on a host machine).
If the host is interested, `is_exclusive` and `observed_keys` can be used to distinguish between a keyboard that is a full keyboard (such as that on a micro) and one that is just a small number of keys (such as a games console with some buttons on the main unit). This can be useful when allowing somebody to control a joystick-driven machine that also has some keys on it with their keyboard — it allows the host to determine that they can use most of the keyboard for joystick input and to learn which keys it cannot use for joystick emulation.
### Providing a target for pasteboard/clipboard text
`KeyboardMachine::Machine`s also offer `type_string`. You can pass a `std::string` directly to it, to instruct the machine to type that phrase next. It is by this route that ports can implement support for pasting text into a machine.
No mechanism is yet provided for copying.
## Providing Joystick Input
If a dynamic machine vends a joystick machine, that machine can provide you with a vector of the joysticks that machine currently believes it has plugged in. The first thing in the array will be the joystick most normally associated with the first player, the second will be that associated with the second, etc. Each thing in the vector will be an instance of `Inputs::Joystick`.
Each joystick provides a list of inputs; broadly inputs are either an axis or a button. More specifically, they can be a full axis or half an axis (e.g. left and right are half axes; 'x' is a whole axis), and buttons can be either fire buttons or keys. Keys tend to be peripheral to main play (e.g. the number pad on a ColecoVision) whereas fire buttons are usually primary inputs.
Both `set_input(input, bool)` and `set_input(input, float)` are provided for pushing state to a joystick.
*A host needn't worry about digital versus analogue input, or axes versus half-axes if it doesn't want to*. Correlated inputs will be converted automatically — e.g. if the user has only an analogue stick then just providing its input as two analogue full axes will cause the proper digital half-axes to be set on machines that accept only digital input, and this is exactly the behaviour that both the Mac and SDL ports currently rely upon.
_Commentary: I'm still not entirely sold on the button/key distinction and how well it would map to all platforms. Some tweaks may occur here._
## Obtaining Audio Output
If a `CRTMachine` (see caveats on this class above) produces audio, it will provide an instance `Outputs::Speaker::Speaker`.
The port should use `get_ideal_clock_rate_in_range` to query what audio output frequency it would most ideally use within a given range, allowing for the underlying machine. Some machines include low-pass filters that make higher rates unnecessary. Nevertheless, `set_output_rate` allows the port to nominate any audio generation rate that is convenient for it, along with the packet size for audio (e.g. whether you want to receive 512 samples at a time, or 64, or 2048, or 115, or any other number).
It should then register a speaker delegate via `set_delegate`. The delegate will receive a call-out (i) any time a new packet of samples is ready; and (ii) in an advisory capacity every time the underlying clock rate that is causing audio to be generated changes. Notifications via (ii) can be ignored if desired, or can be used to trigger a renegotiation of the output rate.
*New samples will be delivered on an arbitrary thread, almost certainly distinct from the one you called run_for on.* This is because audio generation happens on a separate thread from machine emulation.
_Commentary: so far, only mono audio is supported. Expect some minor modifications to the delegate protocol here._
## Obtaining Video Output
Via `CRTMachine` (see above), the host can provide a `ScanTarget`. A scan target is something that receives 'scans', i.e. individual runs of one-dimensional graphical output, on a continuous 2d canvas. As per the `CRTMachine` moniker, historically these always emanated from the CRT emulation.
See `Outputs::ScanTarget` for the full scan target interface. In approximate terms, you're going to receive both some information about decoded sync pulses (horizontal and vertical) plus the 2d start and end points of individual scans, and some negotiation over storage for the 1d data that goes inside them — the scan target is responsible for management of data storage, to allow it to be pushed into a GPU/CPU shared memory area on machines that support that. Things like colour clock phase are provided per scan and at pulse boundaries.
It also receives various modal properties, including the type of data that is being forwarded (various forms of pure luminance, luminance plus phase or full-on RGB), the various scales at play, and the colour space and gamma.
*Both current ports opt to use the same pre-packaged ScanTarget*, which bundles all of that information off to OpenGL, and provides proper composite and S-Video decoders. _Commentary: and further scan targets are likely to arrive by factoring buffer management out of there._
### The OpenGL ScanTarget
See `Outputs/OpenGL/ScanTarget.hpp`. This is a scan target suitable for provision to an emulated machine that will collect the output it is given and then provides (i) an update method, to process as much information as it has recently received; and (ii) a draw method, to draw its current state to the display.
It is thread safe; either or both of update and draw can be called on any thread, whether emulation is currently ongoing or not.
The two things are distinct since it is useful for some ports to be able to redraw the current state without the costs of mutating it, e.g. when interactively resizing a window.
*This particular scan target currently requires desktop OpenGL 3.2.*
Make sure that when you instantiate your instance of the OpenGL scan target, or ask it to draw or update, that there is an active OpenGL context as those calls all result in GPU activity.
## Observing 'Activity'
This is a nice-to-have for a port, but very optional. Some machines provide activity LEDs, e.g. for drive activity or for caps lock. If the dynamic machine can vend an activity source then an activity observer can be posted to it. That observer will immediately receive an enumeration of LEDs and drives attached to the system, and will subsequently receive notification of both audible drive events (i.e. activity a user could observe) and LED changes.
## Providing Time Revisited: the Best-Effort Updater
The best-effort updater, `Concurrency/BestEffortUpdater.hpp` is a helper class currently used by both the Mac and SDL ports, though it's entirely optional. It provides a thread-safe `update` method that can be called to request that 'more' emulation occurs. It will tell a delegate how long to run emulation for, but it will do so only if a call to the delegate isn't already happening.
So the scheme used by both ports is simply to call `update` any time that more output — video or audio — is required, on whichever thread it learns that.