Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions VisualPinball.Engine.PinMAME.Test/PinMameTimeFenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using NUnit.Framework;

namespace VisualPinball.Engine.PinMAME.Test
{
public class PinMameTimeFenceTests
{
[TestCase(0)]
[TestCase(1800)]
public void MenuTimeDoesNotAdvanceRom(double applicationTime)
{
var clock = new PinMameTimeFence();
Assert.That(clock.ToEmulationTime(applicationTime), Is.EqualTo(0.01).Within(1e-9));
Assert.That(clock.ToEmulationTime(applicationTime + 1), Is.EqualTo(1.01).Within(1e-9));
}

[Test]
public void BackwardsClockNeverDisablesFence()
{
var clock = new PinMameTimeFence();
clock.ToEmulationTime(100);
Assert.That(clock.ToEmulationTime(99), Is.EqualTo(0.01));
}

[Test]
public void ReloadResetsRomClockAndPauseKeepsFenceEnabled()
{
var clock = new PinMameTimeFence();
clock.ToEmulationTime(100);
Assert.That(clock.ToEmulationTime(105), Is.EqualTo(5.01).Within(1e-9));
Assert.That(clock.ToEmulationTime(105), Is.EqualTo(5.01).Within(1e-9));
clock.Reset();
Assert.That(clock.ToEmulationTime(300), Is.EqualTo(0.01).Within(1e-9));
Assert.That(clock.ToEmulationTime(300.001), Is.EqualTo(0.011).Within(1e-9));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
<Compile Include="..\VisualPinball.Engine.PinMAME.Unity\Editor\PinMameSpatialMatcher.cs" Link="PinMameSpatialMatcher.cs" />
<Compile Include="..\VisualPinball.Engine.PinMAME.Unity\Runtime\PinMameLegacyAliasResolver.cs" Link="PinMameLegacyAliasResolver.cs" />
<Compile Include="..\VisualPinball.Engine.PinMAME.Unity\Runtime\PinMameOutputModel.cs" Link="PinMameOutputModel.cs" />
<Compile Include="../VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameTimeFence.cs" Link="PinMameTimeFence.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,39 @@ public void SetAvailableHardware(IEnumerable<GamelogicEngineSwitch> switches, IE
private const int MaxLoggedChangedLampSamples = 24;
private const int MaxLoggedUnmappedLampSamples = 24;

private readonly PinMameTimeFence _timeFenceClock = new();
private readonly object _timeFenceLock = new();
private bool _timeFenceActive;

public void SetTimeFence(double timeInSeconds)
{
lock (_timeFenceLock) {
if (!_timeFenceActive || Volatile.Read(ref _stopRequested) != 0) {
return;
}
SetNativeTimeFence(_timeFenceClock.ToEmulationTime(timeInSeconds));
}
}

// Serialize reset, tick and shutdown so no in-flight tick can re-enable a stopped fence.
private void ResetTimeFence()
{
lock (_timeFenceLock) {
_timeFenceClock.Reset();
_timeFenceActive = true;
SetNativeTimeFence(PinMameTimeFence.InitialFenceSeconds);
}
}

private void DisableTimeFence()
{
lock (_timeFenceLock) {
_timeFenceActive = false;
SetNativeTimeFence(0.0);
}
}

private void SetNativeTimeFence(double timeInSeconds)
{
if (_pinMame == null) {
return;
Expand Down Expand Up @@ -760,7 +792,7 @@ public void StopForPlayModeExit()
// Do not call into native Stop() here (it blocks); the editor hook will stop PinMAME
// after we unsubscribe managed callbacks.
Interlocked.Exchange(ref _stopRequested, 1);
SetTimeFence(0.0);
DisableTimeFence();
// Stop polling outputs immediately to avoid concurrent calls into pinmame.dll
// while a stop is in progress (pinmame APIs are not guaranteed thread-safe).
_isRunning = false;
Expand Down Expand Up @@ -798,7 +830,7 @@ private void RequestStopGame(string reason)
#endif

try {
SetTimeFence(0.0);
DisableTimeFence();

if (Interlocked.Exchange(ref _stopRequested, 1) != 0) {
return;
Expand Down Expand Up @@ -985,7 +1017,7 @@ public async Task OnInit(Player player, TableApi tableApi, BallManager ballManag
_pinMame.SetHandleKeyboard(false);
_pinMame.SetHandleMechanics(DisableMechs ? 0 : 0xFF);
ApplyDmdModePreference();
SetTimeFence(0.01);
ResetTimeFence();

_pinMame.OnGameStarted += OnGameStarted;
_pinMame.OnGameEnded += OnGameEnded;
Expand Down Expand Up @@ -1017,8 +1049,10 @@ public async Task OnInit(Player player, TableApi tableApi, BallManager ballManag
try {
// the game stopped here isn't ours, so its end isn't a failed start
_awaitingGameStart = false;
DisableTimeFence();
_pinMame.StopGame();
await Task.Delay(100, ct);
ResetTimeFence();
_awaitingGameStart = true;
_pinMame.StartGame(romId);
Logger.Debug($"[PinMAME] StartGame retry returned. RunState={PinMame.PinMame.RunState}");
Expand Down
18 changes: 18 additions & 0 deletions VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameTimeFence.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace VisualPinball.Engine.PinMAME
{
/// <summary>Maps the application's simulation clock to the current ROM's elapsed time.</summary>
internal sealed class PinMameTimeFence
{
// Zero disables PinMAME's fence; retain the startup fence on the first tick.
internal const double InitialFenceSeconds = 0.01;
private double? _origin;

internal void Reset() => _origin = null;

internal double ToEmulationTime(double simulationSeconds)
{
_origin ??= simulationSeconds;
return System.Math.Max(InitialFenceSeconds, simulationSeconds - _origin.Value + InitialFenceSeconds);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading