Como dices, la opción -soundsync funciona en cualquier situación en la que exista desfase entre la velocidad original y la emulada, independientemente de la opción de sincrozación de vídeo que se utilice. Su funcionamiento es bastante rudimentario, básicamente consiste en aplicar un coeficiente a la frecuencia de muestreo del buffer de audio, calculado como velocidad-real/velocidad-emulada, que se actualiza constantemente.
Respecto a tus pruebas con MAME UI FX 0.136, parece que en los dos casos que comentas estaríamos tan cerca del refresco original (el error es de centésimas de Hz) que el problema no llega a manifestarse porque el desfase es absorbido. He revisado los parches de MAME UI FX y no actúan sobre las opciones de sincronización, por lo que descarto que en ese aspecto sea "mejor" que la versión normal. De todos modos, el dato correcto de refresco de un modo de vídeo debes obtenerlo mediante la medición que realiza Arcade_OSD el pulsar "5", en vez de usar el valor que aparece en ModeList.txt, especialmente si ya no utilizas la R9250 (en ese caso sí que son prácticamente coincidentes dado que medí uno por uno los valores reales de 'dotclock' de esa tarjeta).
A poco que te alejes de los valores originales de refresco vertical, empezarán a aparecer los problemas de hipos, pero paradójicamente, sólo si la frecuencia obtenida es mayor que la original. Esto se sabe desde hace tiempo, y de hecho, en cierto punto los modelines de la AVGA se rectificaron para que su refresco fuera ligeramente menor de 60Hz, para evitar hipos en el scroll de la mayoría de los juegos. Luego intentaré explicar por qué esto es así.
No obstante lo deseable sería que -syncrefresh ajustase la acción del juego a la frecuencia de la tarjeta de vídeo cualquiera que ésta fuera, porque no siempre podemos contar con obtener una aproximación tan buena como la de tu ejemplo, y al final hay muchos casos especiales a los que dar respuesta.
Lo que pongo a continuación es un comentario extraído del propio código de Mame, donde se explica en que consiste el mecanismo de "throttling" que implementa el emulador:
\src\emu\video.c
Throttling theory:
This routine is called periodically with an up-to-date emulated time.
The idea is to synchronize real time with emulated time. We do this
by "throttling", or waiting for real time to catch up with emulated
time.
In an ideal world, it will take less real time to emulate and render
each frame than the emulated time, so we need to slow things down to
get both times in sync.
There are many complications to this model:
* some games run too slow, so each frame we get further and
further behind real time; our only choice here is to not
throttle
* some games have very uneven frame rates; one frame will take
a long time to emulate, and the next frame may be very fast
* we run on top of multitasking OSes; sometimes execution time
is taken away from us, and this means we may not get enough
time to emulate one frame
* we may be paused, and emulated time may not be marching
forward
* emulated time could jump due to resetting the machine or
restoring from a saved state
Supongamos, para simplicar las cosas, que Mame es capaz de emular un ciclo completo en un intervalo de tiempo despreciable. Esto es: procesar el "input", emular el hardware y generar un nuevo frame, todo, en un instante. El resto del tiempo disponible tendremos que esperar, si no queremos que nuestro fps se dispare hasta el infinito.
Esta es la diferencia principal entre la emulación en general, donde normalmente tenemos que respetar el valor fps fijo, y los juegos 3D en tiempo real, donde el valor fps es arbitrario y sólo limitado por la potencia de la CPU/GPU, que es el enfoque que se explica en el artículo sobre triplebuffering que enlazaste al principio del hilo.
Para entender mejor cómo se organiza esto aquí va un esquema de la rutina que ejecuta Mame cada vez que tiene listo un frame:
video_manager::frame_update
{
.
.
.
update_throttle -> aquí se produce el primer retardo, mientras esperamos que transcurran tantos ciclos del reloj-CPU como sean necesarios para sincronizar el tiempo-real con el tiempo-emulado.
.
.
.
m_machine.osd().update -> aquí se produce el segundo retardo, mientras esperamos al inicio del 'blanking' vertical para dibujar mostrar el nuevo frame.
.
.
.
}
Lo primero que llama la atención es que el retardo (update_throttle) se ejecuta antes de que Mame muestre el frame, lo cual no parece lógico, aunque como veremos tiene poca o nula repercusión en la práctica.
El hecho de tener dos retardos no significa que ambos se lleguen a producir íntegramente. Lo que realmente ocurre es que, transcurridos unos cuantos ciclos, la suma de los dos retardos (throttle + waitvsync) tiende a ser constante, de forma que throttle se autoregula esperando sólamente el tiempo necesario para conseguir la velocidad deseada.
Consideremos primero el caso en que el refresco de la tarjeta de vídeo sea menor que el refresco original. Esto significa que el intervalo de tiempo empleado en m_machine.osd().update, esto es, esperando el blanking vertical, es mayor que el intervalo original del juego. Por tanto, tras unos pocos ciclos, update_throttle detectará que vamos más despacio que la velocidad original, por tanto cada llamada a update_throttle retornará inmediatamente sin esperar, realizándose sólo la espera propia a waitvsync dentro de m_machine.osd().update. Esto hace que la velocidad de emulación se ajuste a la velocidad de refresco impuesta por la tarjeta gráfica, resultando en un scroll perfecto.
Ahora consideremos el caso en que el refresco de la tarjeta de vídeo es mayor que el refresco original. Ahora, el tiempo empleado esperando al blaking vertical es inferior al intervalo total de retardo que necesitamos para mantener el valor de fps original. Por tanto, update_throttle detectará que vamos más deprisa, y por tanto intentará compensar este desfase con una espera adicional. Por otra parte, el blanking vertical es una breve ventana temporal que se produce en intervalos perfectamente regulares, y que hemos de aprovechar para escribir en el buffer de vídeo. El problema viene porque el pequeño retardo añadido por update_throttle, se va acumulando tras varios frames, hasta que llega un momento en que llegamos demasiado tarde para entrar en el blanking que nos tocaba, y tenemos que esperar al siguiente, lo que visualmente se traduce en un hipo de scroll imposible de ignorar.
Resulta evidente pues que la única forma de conseguir la sincronización perfecta en ambas situaciones es desactivar manualmente el mecanismo de throttling. El problema, como hemos comentado otras veces, es que al hacer esto, Mame interpreta que queremos que funcione a toda velocidad, por lo que ignora el retardo waitvsync. Esto es lo que corregimos en GroovyMame.