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
103 changes: 103 additions & 0 deletions Brovan/Android/AndroidAudioSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Runtime.InteropServices;
using Brovan.Core.Emulation.OS.SharedHelpers;

namespace Brovan.Android
{
internal sealed unsafe class AndroidAudioSink : IAudioSink
{
private const int FormatPcmFloat = 2;
private const int PerformanceModeLowLatency = 12;
private const long WriteTimeoutNanoseconds = 1_000_000_000;

[DllImport("libaaudio.so")]
private static extern int AAudio_createStreamBuilder(out IntPtr Builder);

[DllImport("libaaudio.so")]
private static extern void AAudioStreamBuilder_setFormat(IntPtr Builder, int Format);

[DllImport("libaaudio.so")]
private static extern void AAudioStreamBuilder_setChannelCount(IntPtr Builder, int Channels);

[DllImport("libaaudio.so")]
private static extern void AAudioStreamBuilder_setSampleRate(IntPtr Builder, int SampleRate);

[DllImport("libaaudio.so")]
private static extern void AAudioStreamBuilder_setPerformanceMode(IntPtr Builder, int Mode);

[DllImport("libaaudio.so")]
private static extern int AAudioStreamBuilder_openStream(IntPtr Builder, out IntPtr Stream);

[DllImport("libaaudio.so")]
private static extern int AAudioStreamBuilder_delete(IntPtr Builder);

[DllImport("libaaudio.so")]
private static extern int AAudioStream_requestStart(IntPtr Stream);

[DllImport("libaaudio.so")]
private static extern int AAudioStream_write(IntPtr Stream, void* Buffer, int Frames, long TimeoutNanoseconds);

[DllImport("libaaudio.so")]
private static extern int AAudioStream_close(IntPtr Stream);

private readonly int BlockAlign;
private IntPtr Stream;

public AndroidAudioSink(AudioSinkFormat Format)
{
BlockAlign = Format.BlockAlign;

if (AAudio_createStreamBuilder(out IntPtr Builder) != 0 || Builder == IntPtr.Zero)
throw new InvalidOperationException("AAudio_createStreamBuilder failed.");

try
{
AAudioStreamBuilder_setFormat(Builder, FormatPcmFloat);
AAudioStreamBuilder_setChannelCount(Builder, Format.Channels);
AAudioStreamBuilder_setSampleRate(Builder, (int)Format.SampleRate);
AAudioStreamBuilder_setPerformanceMode(Builder, PerformanceModeLowLatency);

if (AAudioStreamBuilder_openStream(Builder, out Stream) != 0 || Stream == IntPtr.Zero)
throw new InvalidOperationException("AAudioStreamBuilder_openStream failed.");
}
finally
{
AAudioStreamBuilder_delete(Builder);
}

AAudioStream_requestStart(Stream);
}

public void Write(ReadOnlySpan<byte> Samples)
{
if (Stream == IntPtr.Zero)
return;

fixed (byte* Base = Samples)
{
int Offset = 0;
while (Offset < Samples.Length)
{
int Frames = (Samples.Length - Offset) / BlockAlign;
if (Frames == 0)
return;

int Written = AAudioStream_write(Stream, Base + Offset, Frames, WriteTimeoutNanoseconds);
if (Written <= 0)
return;

Offset += Written * BlockAlign;
}
}
}

public void Dispose()
{
if (Stream == IntPtr.Zero)
return;

AAudioStream_close(Stream);
Stream = IntPtr.Zero;
}
}
}
5 changes: 5 additions & 0 deletions Brovan/Android/AndroidWinManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ public void Present()
{
}

// Touch input has no pointer to move; the guest's own cursor position is authoritative here.
public void WarpCursor(int clientX, int clientY)
{
}

public void Show() => Visible = true;

public void Hide() => Visible = false;
Expand Down
74 changes: 48 additions & 26 deletions Brovan/Core/Emulation/BinaryEmulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ public int Compare(MemoryRegion x, MemoryRegion y)
private ulong _timestampCounter = 0x100000000UL;

private const ulong TscCyclesPerInstruction = 3;

/// <summary>
/// How long the scheduler blocks in one go when no guest thread can run.
/// </summary>
private const int IdleWaitSliceMs = 5;

private const ulong TscCyclesPerMillisecond = 3_000_000UL;
private const ulong RdtscReadCycles = 60;
private const ulong RdtscpReadCycles = 90;
Expand Down Expand Up @@ -451,6 +457,26 @@ internal bool IsEmulatedDeadlineExpired(long Deadline)
return Deadline != -1 && EmulatedTickCount64 >= Deadline;
}

/// <summary>
/// The performance counter, on the same timebase as <see cref="EmulatedTickCount64"/> and at finer
/// resolution than it.
/// </summary>
internal ulong GetEmulatedPerformanceCounter()
{
const long QpcFrequency = OS.Windows.KuserSharedDataManager.QpcFrequency;

long HostTicks = _wallClock.ElapsedTicks;
long HostFrequency = System.Diagnostics.Stopwatch.Frequency;
long Elapsed = HostFrequency == QpcFrequency
? HostTicks
: (long)((decimal)HostTicks * QpcFrequency / HostFrequency);

long Skew = Volatile.Read(ref _emulatedTimeSkewMilliseconds);
long SkewCounts = Skew > long.MaxValue / (QpcFrequency / 1000) ? long.MaxValue : Skew * (QpcFrequency / 1000);

return unchecked((ulong)(Elapsed > long.MaxValue - SkewCounts ? long.MaxValue : Elapsed + SkewCounts));
}

/// <summary>
/// Advances guest time for a wait that was not served in real time.
/// </summary>
Expand All @@ -474,6 +500,10 @@ internal void AdvanceEmulatedTimeMilliseconds(long Milliseconds, bool AdvanceTim
else
_timestampCounter += Ticks * TscCyclesPerMillisecond;
}

// A skew jump is the one moment the page is guaranteed stale, and the guest usually reads it
// immediately afterwards: the wait it was serving has just come due.
WinHelper?.KuserSharedData?.RefreshIfUnhooked();
}

/// <summary>
Expand Down Expand Up @@ -2246,7 +2276,11 @@ private bool TryGetNextWaitSleepMs(out int SleepMs, int MaxSleepMs = 10)
continue;

long Delta = Thread.WaitDeadline - Now;
if (Delta > 0 && Delta < BestDelta)

if (Delta <= 0)
return true;

if (Delta < BestDelta)
BestDelta = Delta;
}

Expand Down Expand Up @@ -2531,19 +2565,17 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels =
if (TryGetNextWaitSleepMs(out int SleepMs, int.MaxValue))
{
if (Debug)
TriggerDebugMessage($"scheduler: no runnable thread, advancing guest time by {SleepMs}ms");
if (HasActiveGetMessageWait())
Thread.Sleep(Math.Min(SleepMs, 16));
AdvanceEmulatedTimeMilliseconds(SleepMs, AdvanceTimestampCounter: true);
TriggerDebugMessage($"scheduler: no runnable thread, waiting up to {SleepMs}ms");
Thread.Sleep(Math.Min(SleepMs, IdleWaitSliceMs));
WinHelper?.KuserSharedData?.RefreshIfUnhooked();
WakeupScanRequired = true;
continue;
}

if (HasActiveGetMessageWait())
{
const int MessagePumpPollMs = 10;
Thread.Sleep(MessagePumpPollMs);
AdvanceEmulatedTimeMilliseconds(MessagePumpPollMs, AdvanceTimestampCounter: true);
Thread.Sleep(IdleWaitSliceMs);
WinHelper?.KuserSharedData?.RefreshIfUnhooked();
WakeupScanRequired = true;
continue;
}
Expand Down Expand Up @@ -2625,30 +2657,20 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels =
ImmaBeEmulatedOOO.InstructionsExecuted += SchedulerSliceWork;
Total += SchedulerSliceWork;
AdvanceTimestampCounter(SchedulerSliceWork);
bool AdvancedEmulatedTime = false;

bool TimedWaitRescanDue = false;
if (SchedulerSliceWork > 1)
{
PendingSchedulerTimeCycles += (ulong)SchedulerSliceWork * TscCyclesPerInstruction;
if (PendingSchedulerTimeCycles >= SchedulerCyclesPerMillisecond)
{
ulong TimeBudgetMs = PendingSchedulerTimeCycles / SchedulerCyclesPerMillisecond;
int MaxAdvanceMs = TimeBudgetMs > int.MaxValue ? int.MaxValue : (int)TimeBudgetMs;

if (TryGetNextWaitSleepMs(out int SliceSleepMs, MaxAdvanceMs))
{
AdvanceEmulatedTimeMilliseconds(SliceSleepMs);
AdvancedEmulatedTime = true;
ulong ConsumedCycles = (ulong)SliceSleepMs * SchedulerCyclesPerMillisecond;
PendingSchedulerTimeCycles = ConsumedCycles >= PendingSchedulerTimeCycles ? 0 : PendingSchedulerTimeCycles - ConsumedCycles;
}
else
{
PendingSchedulerTimeCycles = 0;
}
PendingSchedulerTimeCycles = 0;
TimedWaitRescanDue = true;
}
}

Slices++;
WinHelper?.KuserSharedData?.RefreshIfUnhooked();
ImmaBeEmulatedOOO.LastRunTick = SchedulerTick;

if (ImmaBeEmulatedOOO.Context?.RIP == 0)
Expand Down Expand Up @@ -2687,11 +2709,11 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels =
KnownThreadOrderCount = ThreadOrder.Count;
}

WakeupScanRequired = SliceRequestedRefresh || AdvancedEmulatedTime || ThreadOrder.Count != KnownThreadOrderCount || ImmaBeEmulatedOOO.State == EmulatedThreadState.Waiting;
WakeupScanRequired = SliceRequestedRefresh || TimedWaitRescanDue || ThreadOrder.Count != KnownThreadOrderCount || ImmaBeEmulatedOOO.State == EmulatedThreadState.Waiting;

if (Debug && (Slices <= 64 || (Slices & 0xFF) == 0 || ImmaBeEmulatedOOO.State != StateBeforeSlice || SliceRequestedRefresh || AdvancedEmulatedTime))
if (Debug && (Slices <= 64 || (Slices & 0xFF) == 0 || ImmaBeEmulatedOOO.State != StateBeforeSlice || SliceRequestedRefresh || TimedWaitRescanDue))
{
TriggerDebugMessage($"scheduler: slice tid={ImmaBeEmulatedOOO.ThreadId} {StateBeforeSlice}->{ImmaBeEmulatedOOO.State} work={SchedulerSliceWork} total={Total} rip=0x{RipBeforeSlice:X}->0x{ImmaBeEmulatedOOO.Context?.RIP ?? 0:X} refresh={SliceRequestedRefresh} advancedTime={AdvancedEmulatedTime} boost={ImmaBeEmulatedOOO.DynamicBoost}");
TriggerDebugMessage($"scheduler: slice tid={ImmaBeEmulatedOOO.ThreadId} {StateBeforeSlice}->{ImmaBeEmulatedOOO.State} work={SchedulerSliceWork} total={Total} rip=0x{RipBeforeSlice:X}->0x{ImmaBeEmulatedOOO.Context?.RIP ?? 0:X} refresh={SliceRequestedRefresh} rescanDue={TimedWaitRescanDue} boost={ImmaBeEmulatedOOO.DynamicBoost}");
}

if (MaxTotalInstructions != 0 && Total >= MaxTotalInstructions)
Expand Down
4 changes: 2 additions & 2 deletions Brovan/Core/Emulation/Guests/WindowsGuest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ public ulong AllocateAndInitializeTEB(BinaryEmulator Instance, EmulatedThread Th
Instance._emulator.WriteMemory(Teb + 0x68, (uint)0u);
Instance._emulator.WriteMemory(Teb + 0x108, (uint)0x0409u);
Instance._emulator.WriteMemory(Teb + 0x1760, (uint)0u);
Instance._emulator.WriteMemory(Teb + 0x179C, (uint)1u);
Instance._emulator.WriteMemory(Teb + 0x179C, (uint)0u);
Instance._emulator.WriteMemory(Teb + 0x17A0, (ulong)0ul);
ushort SameTebFlags = 0;
if (InitialThread)
Expand Down Expand Up @@ -1169,7 +1169,7 @@ public byte[] BuildEnvironment(BinaryEmulator Instance, out ulong size)
if (string.IsNullOrEmpty(Name))
continue;

if (!Name.StartsWith("DXVK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("VK_", StringComparison.OrdinalIgnoreCase))
if (!Name.StartsWith("DXVK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("VK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("SDL_", StringComparison.OrdinalIgnoreCase))
continue;

Env[Name] = HostVariable.Value as string ?? string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Diagnostics;
using System.Threading;

namespace Brovan.Core.Emulation.OS.SharedHelpers
{
/// <summary>
/// A host playback device. <see cref="Write"/> blocks until the device has taken the samples.
/// </summary>
public interface IAudioSink : IDisposable
{
void Write(ReadOnlySpan<byte> Samples);
}

public readonly struct AudioSinkFormat
{
public readonly uint SampleRate;
public readonly ushort Channels;
public readonly ushort BitsPerSample;

public AudioSinkFormat(uint SampleRate, ushort Channels, ushort BitsPerSample)
{
this.SampleRate = SampleRate;
this.Channels = Channels;
this.BitsPerSample = BitsPerSample;
}

public int BlockAlign => Channels * (BitsPerSample / 8);

public int BytesPerSecond => (int)SampleRate * BlockAlign;
}

public static class AudioSinkFactory
{
public static IAudioSink Create(AudioSinkFormat Format, out string Backend)
{
try
{
if (Android.AndroidHost.IsActive)
{
Backend = "AAudio";
return new Android.AndroidAudioSink(Format);
}

if (OperatingSystem.IsWindows())
{
Backend = "waveOut";
return new WindowsAudioSink(Format);
}

if (OperatingSystem.IsLinux())
{
Backend = "ALSA";
return new LinuxAudioSink(Format);
}
}
catch (Exception)
{
}

Backend = "silent";
return new SilentAudioSink(Format);
}
}

/// <summary>
/// Used when the host has no usable device. It still has to consume at real time, because the guest's
/// ring only drains as fast as the sink accepts.
/// </summary>
internal sealed class SilentAudioSink : IAudioSink
{
private readonly AudioSinkFormat Format;
private readonly Stopwatch Elapsed = Stopwatch.StartNew();
private long WrittenBytes;

public SilentAudioSink(AudioSinkFormat Format)
{
this.Format = Format;
}

public void Write(ReadOnlySpan<byte> Samples)
{
WrittenBytes += Samples.Length;

long DueMs = WrittenBytes * 1000 / Format.BytesPerSecond;
long BehindMs = DueMs - Elapsed.ElapsedMilliseconds;
if (BehindMs > 0)
Thread.Sleep((int)Math.Min(BehindMs, 100));
}

public void Dispose()
{
}
}
}
Loading
Loading