Initial sound emulation for WPF and XNA+Windows platforms via COM interop to DirectSound.

Added rudimentary UI to Silverlight and WPF platforms to enable disk selection at runtime.

--HG--
extra : convert_revision : svn%3Affd33b8c-2492-42e0-bdc5-587b920b7d6d/trunk%4024459
This commit is contained in:
Sean Fausett 2009-07-26 23:22:00 +00:00
parent 7c2048f0c3
commit 8382195feb
68 changed files with 1010 additions and 339 deletions

189
Library/DirectSound.cs Normal file
View File

@ -0,0 +1,189 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Threading;
namespace Jellyfish.Library
{
public sealed class DirectSoundUpdateEventArgs : EventArgs
{
private DirectSoundUpdateEventArgs()
{
}
public static DirectSoundUpdateEventArgs Create(IntPtr buffer, int bufferSize)
{
_instance.Buffer = buffer;
_instance.BufferSize = bufferSize;
return _instance; // use singleton; avoids garbage
}
public IntPtr Buffer { get; private set; }
public int BufferSize { get; private set; }
private static readonly DirectSoundUpdateEventArgs _instance = new DirectSoundUpdateEventArgs();
}
public sealed partial class DirectSound : IDisposable
{
public DirectSound(int sampleRate, int sampleChannels, int sampleBits, int sampleSize)
{
_sampleRate = sampleRate;
_sampleChannels = sampleChannels;
_sampleBits = sampleBits;
_sampleSize = sampleSize;
_thread = new Thread(Run) { Name = "DirectSound" };
}
public void Dispose()
{
_position1Event.Close();
_position2Event.Close();
_stopEvent.Close();
}
public void Start(IntPtr window)
{
_window = window;
_thread.Start();
}
public void Stop()
{
_stopEvent.Set();
_thread.Join();
}
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Runtime.InteropServices.SafeHandle.DangerousGetHandle")]
private void Initialize()
{
int hresult = NativeMethods.DirectSoundCreate(IntPtr.Zero, out _device, IntPtr.Zero);
if (hresult < 0)
{
Marshal.ThrowExceptionForHR(hresult);
}
_device.SetCooperativeLevel(_window, CooperativeLevel.Normal);
GCHandleHelpers.Pin(new WaveFormat(_sampleRate, _sampleChannels, _sampleBits), waveFormat =>
{
BufferDescription description = new BufferDescription(BufferCapabilities.CtrlPositionNotify, BlockCount * _sampleSize, waveFormat);
_device.CreateSoundBuffer(description, out _buffer, IntPtr.Zero);
});
ClearBuffer();
BufferPositionNotify[] positionEvents = new BufferPositionNotify[BlockCount]
{
new BufferPositionNotify(0 * _sampleSize, _position1Event.SafeWaitHandle.DangerousGetHandle()),
new BufferPositionNotify(1 * _sampleSize, _position2Event.SafeWaitHandle.DangerousGetHandle())
};
((IDirectSoundNotify)_buffer).SetNotificationPositions(positionEvents.Length, positionEvents);
_buffer.Play(0, 0, BufferPlay.Looping);
}
private void ClearBuffer()
{
UpdateBuffer(0, 0, BufferLock.EntireBuffer, (buffer, bufferSize) =>
{
MarshalHelpers.ZeroMemory(buffer, bufferSize);
});
}
private void RestoreBuffer()
{
BufferStatus status;
_buffer.GetStatus(out status);
if ((status & BufferStatus.BufferLost) != 0)
{
_buffer.Restore();
}
}
private void UpdateBuffer(int block)
{
EventHandler<DirectSoundUpdateEventArgs> handler = Update;
if (handler != null)
{
UpdateBuffer(block * _sampleSize, _sampleSize, BufferLock.None, (buffer, bufferSize) =>
{
handler(this, DirectSoundUpdateEventArgs.Create(buffer, bufferSize));
});
}
}
private void UpdateBuffer(int offset, int count, BufferLock flags, Action<IntPtr, int> updateBuffer)
{
RestoreBuffer();
IntPtr buffer1, buffer2;
int buffer1Size, buffer2Size;
_buffer.Lock(offset, count, out buffer1, out buffer1Size, out buffer2, out buffer2Size, flags);
try
{
if (buffer1 != IntPtr.Zero)
{
updateBuffer(buffer1, buffer1Size);
}
if (buffer2 != IntPtr.Zero)
{
updateBuffer(buffer2, buffer2Size);
}
}
finally
{
_buffer.Unlock(buffer1, buffer1Size, buffer2, buffer2Size);
}
}
private void Uninitialize()
{
if (_buffer != null)
{
_buffer.Stop();
Marshal.ReleaseComObject(_buffer);
}
if (_device != null)
{
Marshal.ReleaseComObject(_device);
}
}
private void Run() // com mta thread
{
Initialize();
EventWaitHandle[] eventHandles = new EventWaitHandle[] { _position1Event, _position2Event, _stopEvent };
int index = WaitHandle.WaitAny(eventHandles);
while (index < BlockCount)
{
UpdateBuffer((index + 1) % BlockCount); // update next block in circular buffer
index = WaitHandle.WaitAny(eventHandles);
}
Uninitialize();
}
public event EventHandler<DirectSoundUpdateEventArgs> Update;
private const int BlockCount = 2;
private const int WaveFormatPcm = 1;
private int _sampleRate;
private int _sampleChannels;
private int _sampleBits;
private int _sampleSize;
private Thread _thread;
private IntPtr _window;
private IDirectSound _device;
private IDirectSoundBuffer _buffer;
private AutoResetEvent _position1Event = new AutoResetEvent(false);
private AutoResetEvent _position2Event = new AutoResetEvent(false);
private ManualResetEvent _stopEvent = new ManualResetEvent(false);
}
}

View File

@ -0,0 +1,150 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Jellyfish.Library
{
public sealed partial class DirectSound
{
[Flags]
private enum BufferCapabilities
{
PrimaryBuffer = 0x00000001,
CtrlPositionNotify = 0x00000100,
StickyFocus = 0x00004000,
GlobalFocus = 0x00008000
}
[Flags]
private enum BufferLock
{
None = 0x00000000,
FromWriteCursor = 0x00000001,
EntireBuffer = 0x00000002
}
[Flags]
private enum BufferPlay
{
Looping = 0x00000001
}
[Flags]
private enum BufferStatus
{
Playing = 0x00000001,
BufferLost = 0x00000002,
Looping = 0x00000004,
Terminated = 0x00000020
}
private enum CooperativeLevel
{
Normal = 1,
Priority = 2
}
[StructLayout(LayoutKind.Sequential)]
private sealed class BufferDescription
{
public BufferDescription(BufferCapabilities capabilities, int size, IntPtr format)
{
dwSize = Marshal.SizeOf(typeof(BufferDescription));
dwFlags = capabilities;
dwBufferBytes = size;
lpwfxFormat = format;
}
public int dwSize;
public BufferCapabilities dwFlags;
public int dwBufferBytes;
public int dwReserved;
public IntPtr lpwfxFormat;
public Guid guid3DAlgorithm;
}
[StructLayout(LayoutKind.Sequential)]
private struct BufferPositionNotify
{
public BufferPositionNotify(int offset, IntPtr notifyEvent)
{
dwOffset = offset;
hEventNotify = notifyEvent;
}
public int dwOffset;
public IntPtr hEventNotify;
}
[StructLayout(LayoutKind.Sequential)]
private sealed class WaveFormat
{
public WaveFormat(int sampleRate, int sampleChannels, int sampleBits)
{
wFormatTag = WaveFormatPcm;
nSamplesPerSec = sampleRate;
nChannels = (short)sampleChannels;
wBitsPerSample = (short)sampleBits;
nBlockAlign = (short)(sampleChannels * sampleBits / 8);
nAvgBytesPerSec = sampleRate * nBlockAlign;
}
public short wFormatTag;
public short nChannels;
public int nSamplesPerSec;
public int nAvgBytesPerSec;
public short nBlockAlign;
public short wBitsPerSample;
public short cbSize;
}
[ComImport, Guid("279AFA83-4981-11CE-A521-0020AF0BE560"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IDirectSound
{
void CreateSoundBuffer(BufferDescription pcDSBufferDesc, [MarshalAs(UnmanagedType.Interface)] out IDirectSoundBuffer pDSBuffer, IntPtr pUnkOuter);
void GetCaps(IntPtr pDSCaps);
void DuplicateSoundBuffer([MarshalAs(UnmanagedType.Interface)] IDirectSoundBuffer pDSBufferOriginal, [MarshalAs(UnmanagedType.Interface)] out IDirectSoundBuffer pDSBufferDuplicate);
void SetCooperativeLevel(IntPtr hwnd, CooperativeLevel dwLevel);
void Compact();
void GetSpeakerConfig(out int dwSpeakerConfig);
void SetSpeakerConfig(int dwSpeakerConfig);
void Initialize([MarshalAs(UnmanagedType.LPStruct)] Guid pcGuidDevice);
}
[ComImport, Guid("279AFA85-4981-11CE-A521-0020AF0BE560"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IDirectSoundBuffer
{
void GetCaps(IntPtr pDSBufferCaps);
void GetCurrentPosition(out int dwCurrentPlayCursor, out int dwCurrentWriteCursor);
void GetFormat(IntPtr pwfxFormat, int dwSizeAllocated, out int dwSizeWritten);
void GetVolume(out int lVolume);
void GetPan(out int lPan);
void GetFrequency(out int dwFrequency);
void GetStatus(out BufferStatus dwStatus);
void Initialize([MarshalAs(UnmanagedType.Interface)] IDirectSound pDirectSound, BufferDescription pcDSBufferDesc);
void Lock(int dwOffset, int dwBytes, out IntPtr pvAudioPtr1, out int dwAudioBytes1, out IntPtr pvAudioPtr2, out int dwAudioBytes2, BufferLock dwFlags);
void Play(int dwReserved1, int dwPriority, BufferPlay dwFlags);
void SetCurrentPosition(int dwNewPosition);
void SetFormat(WaveFormat pcfxFormat);
void SetVolume(int lVolume);
void SetPan(int lPan);
void SetFrequency(int dwFrequency);
void Stop();
void Unlock(IntPtr pvAudioPtr1, int dwAudioBytes1, IntPtr pvAudioPtr2, int dwAudioBytes2);
void Restore();
}
[ComImport, Guid("B0210783-89CD-11D0-AF08-00A0C925CD16"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IDirectSoundNotify
{
void SetNotificationPositions(int dwPositionNotifies, [MarshalAs(UnmanagedType.LPArray)] BufferPositionNotify[] pcPositionNotifies);
}
[SuppressUnmanagedCodeSecurity]
private static class NativeMethods
{
[DllImport("dsound.dll")]
public static extern int DirectSoundCreate(IntPtr pcGuidDevice, [MarshalAs(UnmanagedType.Interface)] out IDirectSound pDS, IntPtr pUnkOuter);
}
}
}

View File

@ -1,19 +0,0 @@
using System.IO;
namespace Jellyfish.Library
{
public static class FileHelpers
{
public static byte[] ReadAllBytes(string path)
{
#if SILVERLIGHT || XBOX || ZUNE
using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
return stream.ReadAllBytes();
}
#else
return File.ReadAllBytes(path);
#endif
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
namespace Jellyfish.Library
{
public static class GCHandleHelpers
{
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public static void Pin(object value, Action<IntPtr> action)
{
GCHandle gcHandle = new GCHandle();
try
{
gcHandle = GCHandle.Alloc(value, GCHandleType.Pinned);
action(gcHandle.AddrOfPinnedObject());
}
finally
{
if (gcHandle.IsAllocated)
{
gcHandle.Free();
}
}
}
}
}

View File

@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Permissions;
using System.Security.Principal;
namespace Jellyfish.Library
@ -9,6 +10,7 @@ namespace Jellyfish.Library
[StructLayout(LayoutKind.Sequential)]
public sealed class SecurityAttributes
{
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public SecurityAttributes()
{
_length = Marshal.SizeOf(typeof(SecurityAttributes));
@ -116,31 +118,21 @@ public void AddAuditRule(GeneralAuditRule rule)
base.AddAuditRule(rule);
}
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public void GetSecurityAttributes(bool inheritable, Action<SecurityAttributes> action)
{
GetSecurityAttributes(this, inheritable, action);
}
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public static void GetSecurityAttributes(ObjectSecurity security, bool inheritable, Action<SecurityAttributes> action)
{
if (security != null)
{
GCHandle gcHandle = new GCHandle();
try
GCHandleHelpers.Pin(security.GetSecurityDescriptorBinaryForm(), securityDescriptor =>
{
gcHandle = GCHandle.Alloc(security.GetSecurityDescriptorBinaryForm(), GCHandleType.Pinned);
SecurityAttributes securityAttributes = new SecurityAttributes();
securityAttributes.SecurityDescriptor = gcHandle.AddrOfPinnedObject();
securityAttributes.InheritHandle = inheritable;
action(securityAttributes);
}
finally
{
if (gcHandle.IsAllocated)
{
gcHandle.Free();
}
}
action(new SecurityAttributes() { SecurityDescriptor = securityDescriptor, InheritHandle = inheritable });
});
}
else if (inheritable)
{

View File

@ -3,7 +3,7 @@
namespace Jellyfish.Library
{
public class Lazy<T> where T : class
public sealed class Lazy<T> where T : class
{
public Lazy(Func<T> initializer)
{

31
Library/MarshalHelpers.cs Normal file
View File

@ -0,0 +1,31 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
namespace Jellyfish.Library
{
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public static class MarshalHelpers
{
public static void FillMemory(IntPtr buffer, int bufferSize, byte value)
{
NativeMethods.FillMemory(buffer, (IntPtr)bufferSize, value);
}
public static void ZeroMemory(IntPtr buffer, int bufferSize)
{
NativeMethods.ZeroMemory(buffer, (IntPtr)bufferSize);
}
[SuppressUnmanagedCodeSecurity]
private static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern void FillMemory(IntPtr destination, IntPtr length, byte fill);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern void ZeroMemory(IntPtr destination, IntPtr length);
}
}
}

View File

@ -6,11 +6,6 @@ namespace Jellyfish.Library
{
public class ApplicationBase : Application
{
public ApplicationBase() :
this(null)
{
}
public ApplicationBase(string name)
{
Name = name;

View File

@ -5,7 +5,7 @@
namespace Jellyfish.Library
{
public partial class FrameRateCounter : UserControl
public sealed partial class FrameRateCounter : UserControl
{
public FrameRateCounter()
{

View File

@ -54,9 +54,6 @@
<Compile Include="..\AssemblyCommentAttribute.cs">
<Link>AssemblyCommentAttribute.cs</Link>
</Compile>
<Compile Include="..\FileHelpers.cs">
<Link>FileHelpers.cs</Link>
</Compile>
<Compile Include="..\GlobalSuppressions.cs">
<Link>GlobalSuppressions.cs</Link>
</Compile>

View File

@ -5,7 +5,7 @@
namespace Jellyfish.Library
{
public class StringFormatConverter : IValueConverter // SL is missing Binding.StringFormat
public sealed class StringFormatConverter : IValueConverter // SL is missing Binding.StringFormat
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

View File

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Security.Permissions;
using System.Text;
using System.Windows;
using System.Windows.Threading;
@ -8,11 +9,7 @@ namespace Jellyfish.Library
{
public class ApplicationBase : Application
{
public ApplicationBase() :
this(null)
{
}
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public ApplicationBase(string name)
{
Name = name;

View File

@ -5,7 +5,7 @@
namespace Jellyfish.Library
{
public partial class FrameRateCounter : UserControl
public sealed partial class FrameRateCounter : UserControl
{
public FrameRateCounter()
{

View File

@ -62,8 +62,14 @@
<Compile Include="..\AssemblyCommentAttribute.cs">
<Link>AssemblyCommentAttribute.cs</Link>
</Compile>
<Compile Include="..\FileHelpers.cs">
<Link>FileHelpers.cs</Link>
<Compile Include="..\DirectSound.cs">
<Link>DirectSound.cs</Link>
</Compile>
<Compile Include="..\DirectSoundInterop.cs">
<Link>DirectSoundInterop.cs</Link>
</Compile>
<Compile Include="..\GCHandleHelpers.cs">
<Link>GCHandleHelpers.cs</Link>
</Compile>
<Compile Include="..\GeneralSecurity.cs">
<Link>GeneralSecurity.cs</Link>
@ -77,6 +83,9 @@
<Compile Include="..\Lazy.cs">
<Link>Lazy.cs</Link>
</Compile>
<Compile Include="..\MarshalHelpers.cs">
<Link>MarshalHelpers.cs</Link>
</Compile>
<Compile Include="..\MathHelpers.cs">
<Link>MathHelpers.cs</Link>
</Compile>
@ -100,6 +109,7 @@
<DependentUpon>FrameRateCounter.xaml</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WindowExtensions.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="FrameRateCounter.xaml">

View File

@ -0,0 +1,14 @@
using System;
using System.Windows;
using System.Windows.Interop;
namespace Jellyfish.Library
{
public static class WindowExtensions
{
public static IntPtr GetHandle(this Window window)
{
return new WindowInteropHelper(window).Handle;
}
}
}

View File

@ -5,13 +5,11 @@
namespace Jellyfish.Library
{
public class FrameRateCounter : DrawableGameComponent
public sealed class FrameRateCounter : DrawableGameComponent
{
public FrameRateCounter(GameBase game) :
base(game)
{
_frameRateBuilder = new StringBuilder(); // cache builder; avoids garbage
FontColor = Color.White;
FontName = "Default";
@ -68,6 +66,6 @@ public override void Update(GameTime gameTime)
private long _elapsedTime;
private int _frameCount;
private int _frameRate;
private StringBuilder _frameRateBuilder;
private StringBuilder _frameRateBuilder = new StringBuilder(); // cache builder; avoids garbage
}
}

View File

@ -7,11 +7,6 @@ namespace Jellyfish.Library
{
public class GameBase : Game
{
public GameBase() :
this(null)
{
}
public GameBase(string name)
{
Name = name;

View File

@ -62,9 +62,6 @@
<Compile Include="..\AssemblyCommentAttribute.cs">
<Link>AssemblyCommentAttribute.cs</Link>
</Compile>
<Compile Include="..\FileHelpers.cs">
<Link>FileHelpers.cs</Link>
</Compile>
<Compile Include="..\GlobalSuppressions.cs">
<Link>GlobalSuppressions.cs</Link>
</Compile>

View File

@ -60,9 +60,6 @@
<Compile Include="..\AssemblyCommentAttribute.cs">
<Link>AssemblyCommentAttribute.cs</Link>
</Compile>
<Compile Include="..\FileHelpers.cs">
<Link>FileHelpers.cs</Link>
</Compile>
<Compile Include="..\GlobalSuppressions.cs">
<Link>GlobalSuppressions.cs</Link>
</Compile>

View File

@ -84,8 +84,14 @@
<Compile Include="..\AssemblyCommentAttribute.cs">
<Link>AssemblyCommentAttribute.cs</Link>
</Compile>
<Compile Include="..\FileHelpers.cs">
<Link>FileHelpers.cs</Link>
<Compile Include="..\DirectSound.cs">
<Link>DirectSound.cs</Link>
</Compile>
<Compile Include="..\DirectSoundInterop.cs">
<Link>DirectSoundInterop.cs</Link>
</Compile>
<Compile Include="..\GCHandleHelpers.cs">
<Link>GCHandleHelpers.cs</Link>
</Compile>
<Compile Include="..\GeneralSecurity.cs">
<Link>GeneralSecurity.cs</Link>
@ -99,6 +105,9 @@
<Compile Include="..\Lazy.cs">
<Link>Lazy.cs</Link>
</Compile>
<Compile Include="..\MarshalHelpers.cs">
<Link>MarshalHelpers.cs</Link>
</Compile>
<Compile Include="..\MathHelpers.cs">
<Link>MathHelpers.cs</Link>
</Compile>

View File

@ -2,7 +2,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;
using Jellyfish.Virtu.Properties;
namespace Jellyfish.Virtu
{
@ -187,6 +186,7 @@ public int Execute()
Opcode = _memory.Read(RPC);
RPC = (RPC + 1) & 0xFFFF;
_executeOpcode[Opcode]();
Cycles += CC;
// System.Diagnostics.Debug.WriteLine(" " + ToString());
@ -3243,6 +3243,7 @@ private void UpdateSettings()
public int EA { get; private set; }
public int CC { get; private set; }
public int Opcode { get; private set; }
public long Cycles { get; private set; }
private Action _updateEvent;

View File

@ -7,7 +7,7 @@ public partial class Cpu
private const int CyclesPerUpdate = 17030;
private const int CyclesPerVSync = 17030;
private const int CyclesPerSecond = 1022730;
private static readonly long TicksPerVSync = TimeSpan.FromSeconds((double)CyclesPerVSync / (double)CyclesPerSecond).Ticks;
private static readonly long TicksPerVSync = TimeSpan.FromSeconds((double)CyclesPerVSync / CyclesPerSecond).Ticks;
private const int OpcodeCount = 256;

View File

@ -11,6 +11,7 @@
<Word>Annunciator</Word>
<Word>Dsk</Word>
<Word>Opcode</Word>
<Word>Unpause</Word>
<Word>Virtu</Word>
<Word>Xna</Word>
<Word>x</Word>

View File

@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Jellyfish.Virtu.Services;
using Jellyfish.Virtu.Settings;
namespace Jellyfish.Virtu
@ -13,13 +12,8 @@ public DiskII(Machine machine) :
public override void Initialize()
{
_storageService = Machine.Services.GetService<StorageService>();
#if WINDOWS
DiskIISettings settings = Machine.Settings.DiskII;
if (settings.Disk1.Name.Length == 0)
{
settings.Disk1.Name = _storageService.GetDiskFile();
}
if (settings.Disk1.Name.Length > 0)
{
_drives[0].InsertDisk(settings.Disk1.Name, settings.Disk1.IsWriteProtected);
@ -28,6 +22,7 @@ public override void Initialize()
{
_drives[1].InsertDisk(settings.Disk2.Name, settings.Disk2.IsWriteProtected);
}
#endif
}
public override void Reset()
@ -42,10 +37,6 @@ public override void Reset()
public override void Uninitialize()
{
Flush();
DiskIISettings settings = Machine.Settings.DiskII; // TODO remove; reset filename to prompt on next start
settings.Disk1.Name = string.Empty;
settings.Disk2.Name = string.Empty;
}
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
@ -227,13 +218,14 @@ private void SetPhase(int address)
}
}
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
public Drive525[] Drives { get { return _drives; } }
private const int Phase0On = 1 << 0;
private const int Phase1On = 1 << 1;
private const int Phase2On = 1 << 2;
private const int Phase3On = 1 << 3;
private StorageService _storageService;
private Drive525[] _drives = new Drive525[] { new Drive525(), new Drive525() };
private int _latch;
private int _phaseStates;

View File

@ -1,9 +1,10 @@
using System;
using System.IO;
using Jellyfish.Library;
namespace Jellyfish.Virtu
{
public class Drive525
public sealed class Drive525
{
public Drive525()
{
@ -14,13 +15,20 @@ public Drive525()
}
public void InsertDisk(string fileName, bool isWriteProtected)
{
using (FileStream stream = File.OpenRead(fileName))
{
InsertDisk(fileName, stream, isWriteProtected);
}
}
public void InsertDisk(string name, Stream stream, bool isWriteProtected)
{
FlushTrack();
// TODO handle null param/empty string for eject, or add Eject()
byte[] fileData = FileHelpers.ReadAllBytes(fileName);
_disk = Disk525.CreateDisk(fileName, fileData, isWriteProtected);
_disk = Disk525.CreateDisk(name, stream.ReadAllBytes(), isWriteProtected);
_trackLoaded = false;
}

View File

@ -1,7 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfish.Library;
using Jellyfish.Virtu.Properties;
using Jellyfish.Virtu.Services;
using Jellyfish.Virtu.Settings;

View File

@ -1,6 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfish.Virtu.Properties;
using Jellyfish.Virtu.Services;
using Jellyfish.Virtu.Settings;

View File

@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System;
using System.Collections.ObjectModel;
using System.Threading;
using Jellyfish.Library;
using Jellyfish.Virtu.Services;
@ -6,11 +7,12 @@
namespace Jellyfish.Virtu
{
public class Machine
public enum MachineState { Stopped = 0, Starting, Running, Pausing, Paused, Stopping }
public sealed class Machine : IDisposable
{
public Machine()
{
Thread = new Thread(Run) { Name = "Machine" };
Events = new MachineEvents();
Services = new MachineServices();
Settings = new MachineSettings();
@ -23,8 +25,15 @@ public Machine()
Cassette = new Cassette(this);
Speaker = new Speaker(this);
Video = new Video(this);
Components = new Collection<MachineComponent> { Cpu, Memory, DiskII, Keyboard, GamePort, Cassette, Speaker, Video };
Thread = new Thread(Run) { Name = "Machine" };
}
public void Dispose()
{
_pausedEvent.Close();
_unpauseEvent.Close();
}
public void Reset()
@ -36,35 +45,64 @@ public void Start()
{
_storageService = Services.GetService<StorageService>();
_storageService.Load(MachineSettings.FileName, stream => Settings.Deserialize(stream));
State = MachineState.Starting;
Thread.Start();
}
public void Pause()
{
State = MachineState.Pausing;
_pausedEvent.WaitOne();
State = MachineState.Paused;
}
public void Unpause()
{
State = MachineState.Running;
_unpauseEvent.Set();
}
public void Stop()
{
_stopPending = true;
State = MachineState.Stopping;
_unpauseEvent.Set();
Thread.Join();
State = MachineState.Stopped;
_storageService.Save(MachineSettings.FileName, stream => Settings.Serialize(stream));
}
private void Run()
private void Run() // machine thread
{
Components.ForEach(component => component.Initialize());
Reset();
State = MachineState.Running;
do
{
do
{
Events.RaiseEvents(Cpu.Execute());
}
while (!_stopPending);
while (State == MachineState.Running);
if (State == MachineState.Pausing)
{
_pausedEvent.Set();
_unpauseEvent.WaitOne();
}
}
while (State != MachineState.Stopping);
Components.ForEach(component => component.Uninitialize());
}
public Thread Thread { get; private set; }
public MachineEvents Events { get; private set; }
public MachineServices Services { get; private set; }
public MachineSettings Settings { get; private set; }
public Collection<MachineComponent> Components { get; private set; }
public MachineState State { get; private set; }
public Cpu Cpu { get; private set; }
public Memory Memory { get; private set; }
public DiskII DiskII { get; private set; }
@ -73,8 +111,13 @@ private void Run()
public Cassette Cassette { get; private set; }
public Speaker Speaker { get; private set; }
public Video Video { get; private set; }
public Collection<MachineComponent> Components { get; private set; }
public Thread Thread { get; private set; }
private AutoResetEvent _pausedEvent = new AutoResetEvent(false);
private AutoResetEvent _unpauseEvent = new AutoResetEvent(false);
private StorageService _storageService;
private bool _stopPending;
}
}

View File

@ -5,7 +5,7 @@
namespace Jellyfish.Virtu
{
public class MachineEvent
public sealed class MachineEvent
{
public MachineEvent(int delta, Action action)
{
@ -22,7 +22,7 @@ public override string ToString()
public Action Action { get; set; }
}
public class MachineEvents
public sealed class MachineEvents
{
public void AddEvent(int delta, Action action)
{
@ -75,7 +75,7 @@ public int FindEvent(Action action)
}
}
return delta;
return 0;
}
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
@ -86,15 +86,21 @@ public void RaiseEvents(int delta)
while (node.Value.Delta <= 0)
{
delta = node.Value.Delta;
node.Value.Action();
RemoveEvent(node);
node = _used.First;
}
}
private void RemoveEvent(LinkedListNode<MachineEvent> node)
{
if (node.Next != null)
{
node.Next.Value.Delta += node.Value.Delta;
}
_used.Remove(node);
_free.AddFirst(node); // cache node; avoids garbage
node = _used.First;
node.Value.Delta += delta;
}
}
private LinkedList<MachineEvent> _used = new LinkedList<MachineEvent>();

View File

@ -6,7 +6,7 @@
namespace Jellyfish.Virtu.Settings
{
public class MachineSettings
public sealed class MachineSettings
{
public MachineSettings()
{
@ -21,8 +21,8 @@ public MachineSettings()
UseGamePort = false,
Key = new KeySettings
{
Joystick0 = new JoystickSettings { UpLeft = 0, Up = 'E', UpRight = 0, Left = 'S', Right = 'F', DownLeft = 0, Down = 'D', DownRight = 0 },
Joystick1 = new JoystickSettings { UpLeft = 0, Up = 'I', UpRight = 0, Left = 'J', Right = 'L', DownLeft = 0, Down = 'K', DownRight = 0 },
Joystick0 = new JoystickSettings { UpLeft = 0, Up = 'I', UpRight = 0, Left = 'J', Right = 'L', DownLeft = 0, Down = 'K', DownRight = 0 },
Joystick1 = new JoystickSettings { UpLeft = 0, Up = 'E', UpRight = 0, Left = 'S', Right = 'F', DownLeft = 0, Down = 'D', DownRight = 0 },
Button0 = 0, Button1 = 0, Button2 = 0
}
};
@ -305,25 +305,25 @@ public void Serialize(Stream stream)
public const string Namespace = "http://schemas.jellyfish.co.nz/virtu/settings";
}
public class CpuSettings
public sealed class CpuSettings
{
public bool Is65C02 { get; set; }
public bool IsThrottled { get; set; }
}
public class DiskSettings
public sealed class DiskSettings
{
public string Name { get; set; }
public bool IsWriteProtected { get; set; }
};
public class DiskIISettings
public sealed class DiskIISettings
{
public DiskSettings Disk1 { get; set; }
public DiskSettings Disk2 { get; set; }
};
public class JoystickSettings
public sealed class JoystickSettings
{
public int UpLeft { get; set; }
public int Up { get; set; }
@ -335,7 +335,7 @@ public class JoystickSettings
public int DownRight { get; set; }
};
public class KeySettings
public sealed class KeySettings
{
public JoystickSettings Joystick0 { get; set; }
public JoystickSettings Joystick1 { get; set; }
@ -344,19 +344,19 @@ public class KeySettings
public int Button2 { get; set; }
};
public class KeyboardSettings
public sealed class KeyboardSettings
{
public bool UseGamePort { get; set; }
public KeySettings Key { get; set; }
}
public class GamePortSettings
public sealed class GamePortSettings
{
public bool UseKeyboard { get; set; }
public KeySettings Key { get; set; }
}
public class ColorSettings
public sealed class ColorSettings
{
public uint Black { get; set; }
public uint DarkBlue { get; set; }
@ -377,7 +377,7 @@ public class ColorSettings
public uint Monochrome { get; set; }
}
public class VideoSettings
public sealed class VideoSettings
{
public bool IsFullScreen { get; set; }
public bool IsMonochrome { get; set; }

View File

@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.3074
// Runtime Version:2.0.50727.4016
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.

BIN
Virtu/Roms/AppleIIe.rom Normal file

Binary file not shown.

BIN
Virtu/Roms/DiskII.rom Normal file

Binary file not shown.

View File

@ -1,9 +1,86 @@
using System;
using System.Linq;
namespace Jellyfish.Virtu.Services
{
public class AudioService
public class AudioService : MachineService
{
public virtual void Update() { }
public AudioService(Machine machine) :
base(machine)
{
}
public void ToggleOutput() // machine thread
{
lock (_lock)
{
long cycles = Machine.Cpu.Cycles;
int toggleDelta = (int)(cycles - _toggleCycles);
_toggleCycles = cycles;
_deltaBuffer[_writeIndex] = toggleDelta;
_writeIndex = (_writeIndex + 1) % DeltaBufferSize;
}
}
protected void Update(int bufferSize, Action<byte[], int> updateBuffer) // audio thread
{
lock (_lock)
{
long cycles = Machine.Cpu.Cycles;
int updateDelta = (int)(cycles - _updateCycles);
_updateCycles = _toggleCycles = cycles; // reset audio frame
if (updateDelta > 0)
{
double bytesPerCycle = (double)bufferSize / (Machine.Settings.Cpu.IsThrottled ? CyclesPerSample : updateDelta);
while (_readIndex != _writeIndex)
{
int deltaSize = (int)(_deltaBuffer[_readIndex] * bytesPerCycle);
if (deltaSize > bufferSize)
{
_deltaBuffer[_readIndex] -= (int)((double)bufferSize / bytesPerCycle);
break;
}
updateBuffer(_isOutputHigh ? SampleHigh : SampleZero, deltaSize);
_isOutputHigh ^= true;
bufferSize -= deltaSize;
_readIndex = (_readIndex + 1) % DeltaBufferSize;
}
updateBuffer(_isOutputHigh ? SampleHigh : SampleZero, bufferSize);
}
else
{
updateBuffer(SampleZero, bufferSize);
}
}
}
public const int SampleRate = 44100; // hz
public const int SampleChannels = 1;
public const int SampleBits = 8;
public const int SampleSize = (int)(SampleRate * Latency / 1000f) * SampleChannels * SampleBits / 8;
private const int CyclesPerSecond = 1022730;
private const int CyclesPerSample = (int)(CyclesPerSecond * Latency / 1000f);
private const int Latency = 40; // ms
private static readonly byte[] SampleHigh = Enumerable.Repeat((byte)0xFF, SampleSize).ToArray();
private static readonly byte[] SampleZero = new byte[SampleSize];
private const int DeltaBufferSize = 8192;
private int[] _deltaBuffer = new int[DeltaBufferSize];
private uint _readIndex;
private uint _writeIndex;
private bool _isOutputHigh;
private long _toggleCycles;
private long _updateCycles;
private object _lock = new object();
}
}

View File

@ -48,9 +48,10 @@ public override string ToString()
private bool _isDown;
}
public class GamePortService
public class GamePortService : MachineService
{
public GamePortService()
public GamePortService(Machine machine) :
base(machine)
{
Paddle0 = Paddle1 = Paddle2 = Paddle3 = 255; // not connected
}

View File

@ -4,7 +4,7 @@
namespace Jellyfish.Virtu.Services
{
public class AsciiKeyEventArgs : EventArgs
public sealed class AsciiKeyEventArgs : EventArgs
{
private AsciiKeyEventArgs()
{
@ -22,8 +22,13 @@ public static AsciiKeyEventArgs Create(int asciiKey)
private static readonly AsciiKeyEventArgs _instance = new AsciiKeyEventArgs();
}
public abstract class KeyboardService
public abstract class KeyboardService : MachineService
{
protected KeyboardService(Machine machine) :
base(machine)
{
}
public abstract bool IsKeyDown(int key);
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
@ -42,7 +47,7 @@ public void WaitForKeyUp()
{
while (IsAnyKeyDown)
{
Thread.Sleep(100);
Thread.Sleep(10);
}
}
@ -50,7 +55,7 @@ public void WaitForResetKeyUp()
{
while (IsResetKeyDown)
{
Thread.Sleep(100);
Thread.Sleep(10);
}
}

View File

@ -0,0 +1,29 @@
using System;
namespace Jellyfish.Virtu.Services
{
public abstract class MachineService : IDisposable
{
protected MachineService(Machine machine)
{
Machine = machine;
}
~MachineService()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
}
protected Machine Machine { get; private set; }
}
}

View File

@ -5,9 +5,9 @@
namespace Jellyfish.Virtu.Services
{
public class MachineServices : IServiceProvider
public sealed class MachineServices : IServiceProvider
{
public void AddService(Type serviceType, object serviceProvider)
public void AddService(Type serviceType, MachineService serviceProvider)
{
if (_serviceProviders.ContainsKey(serviceType))
{
@ -37,6 +37,6 @@ public void RemoveService(Type serviceType)
_serviceProviders.Remove(serviceType);
}
private Dictionary<Type, object> _serviceProviders = new Dictionary<Type, object>();
private Dictionary<Type, MachineService> _serviceProviders = new Dictionary<Type, MachineService>();
}
}

View File

@ -1,13 +1,15 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Jellyfish.Virtu.Services
{
public abstract class StorageService
public abstract class StorageService : MachineService
{
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
public abstract string GetDiskFile();
protected StorageService(Machine machine) :
base(machine)
{
}
public abstract void Load(string path, Action<Stream> reader);
public abstract void Save(string path, Action<Stream> writer);
}

View File

@ -1,9 +1,12 @@
using System;
namespace Jellyfish.Virtu.Services
namespace Jellyfish.Virtu.Services
{
public abstract class VideoService
public abstract class VideoService : MachineService
{
protected VideoService(Machine machine) :
base(machine)
{
}
public abstract void SetPixel(int x, int y, uint color);
public abstract void Update();

View File

@ -62,6 +62,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Net" />
<Reference Include="System.Windows.Controls.Toolkit, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="System.Xml" />
<Reference Include="System.Windows.Browser" />
<Reference Include="System.Xml.Linq, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
@ -130,6 +131,9 @@
<Compile Include="..\Services\KeyboardService.cs">
<Link>Services\KeyboardService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineService.cs">
<Link>Services\MachineService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineServices.cs">
<Link>Services\MachineServices.cs</Link>
</Compile>
@ -155,6 +159,7 @@
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\SilverlightAudioService.cs" />
<Compile Include="Services\SilverlightKeyboardService.cs" />
<Compile Include="Services\SilverlightStorageService.cs" />
<Compile Include="Services\SilverlightVideoService.cs" />

View File

@ -2,7 +2,7 @@
namespace Jellyfish.Virtu
{
public partial class MainApp : ApplicationBase
public sealed partial class MainApp : ApplicationBase
{
public MainApp() :
base("Virtu")

View File

@ -1,11 +1,18 @@
<UserControl x:Class="Jellyfish.Virtu.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:jl="clr-namespace:Jellyfish.Library;assembly=Jellyfish.Library"
xmlns:jv="clr-namespace:Jellyfish.Virtu;assembly=Jellyfish.Virtu">
<Grid Background="Black" Cursor="None">
<Image Name="_image" MinWidth="560" MinHeight="384"/>
<jl:FrameRateCounter/>
<tk:DockPanel Background="Black">
<StackPanel Orientation="Horizontal" tk:DockPanel.Dock="Top">
<Button x:Name="_disk1Button" Content="Disk 1" IsTabStop="false" Margin="4 4 0 4"/>
<Button x:Name="_disk2Button" Content="Disk 2" IsTabStop="false" Margin="4 4 0 4"/>
<jl:FrameRateCounter Margin="4 4 0 4" VerticalAlignment="Center"/>
</StackPanel>
<Grid Cursor="None">
<Image x:Name="_image" MinWidth="560" MinHeight="384"/>
<MediaElement x:Name="_media"/>
</Grid>
</tk:DockPanel>
</UserControl>

View File

@ -1,60 +1,78 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Jellyfish.Virtu.Services;
using System.Windows.Input;
namespace Jellyfish.Virtu
{
public partial class MainPage : UserControl
public sealed partial class MainPage : UserControl, IDisposable
{
public MainPage()
{
InitializeComponent();
_storageService = new SilverlightStorageService(this);
_keyboardService = new SilverlightKeyboardService(this);
_gamePortService = new GamePortService(); // not connected
_audioService = new AudioService(); // not connected
_videoService = new SilverlightVideoService(_image);
_storageService = new SilverlightStorageService(_machine);
_keyboardService = new SilverlightKeyboardService(_machine, this);
_gamePortService = new GamePortService(_machine); // not connected
_audioService = new SilverlightAudioService(_machine, this, _media);
_videoService = new SilverlightVideoService(_machine, _image);
_machine = new Machine();
_machine.Services.AddService(typeof(StorageService), _storageService);
_machine.Services.AddService(typeof(KeyboardService), _keyboardService);
_machine.Services.AddService(typeof(GamePortService), _gamePortService);
_machine.Services.AddService(typeof(AudioService), _audioService);
_machine.Services.AddService(typeof(VideoService), _videoService);
Loaded += MainPage_Loaded;
Loaded += (sender, e) => _machine.Start();
CompositionTarget.Rendering += CompositionTarget_Rendering;
Application.Current.Exit += MainApp_Exit;
Application.Current.Exit += (sender, e) => _machine.Stop();
_disk1Button.Click += (sender, e) => DiskButton_Click(0);
_disk2Button.Click += (sender, e) => DiskButton_Click(1);
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
public void Dispose()
{
_machine.Start();
_machine.Dispose();
_storageService.Dispose();
_keyboardService.Dispose();
_gamePortService.Dispose();
_audioService.Dispose();
_videoService.Dispose();
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
_keyboardService.Update();
_gamePortService.Update();
_audioService.Update();
_videoService.Update();
}
private void MainApp_Exit(object sender, EventArgs e)
private void DiskButton_Click(int drive)
{
_machine.Stop();
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "Disk Files (*.dsk;*.nib)|*.dsk;*.nib|All Files (*.*)|*.*";
bool? result = dialog.ShowDialog();
if (result.HasValue && result.Value)
{
using (FileStream stream = dialog.File.OpenRead())
{
_machine.Pause();
_machine.DiskII.Drives[drive].InsertDisk(dialog.File.Name, stream, false);
_machine.Unpause();
}
}
}
private Machine _machine = new Machine();
private StorageService _storageService;
private KeyboardService _keyboardService;
private GamePortService _gamePortService;
private AudioService _audioService;
private VideoService _videoService;
private Machine _machine;
}
}

View File

@ -0,0 +1,19 @@
using System.Windows.Controls;
namespace Jellyfish.Virtu.Services
{
public sealed class SilverlightAudioService : AudioService
{
public SilverlightAudioService(Machine machine, UserControl page, MediaElement media) :
base(machine)
{
_page = page;
_media = media;
// TODO
}
private UserControl _page;
private MediaElement _media;
}
}

View File

@ -2,7 +2,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Input;
@ -10,13 +9,14 @@ namespace Jellyfish.Virtu.Services
{
public sealed class SilverlightKeyboardService : KeyboardService
{
public SilverlightKeyboardService(UserControl page)
public SilverlightKeyboardService(Machine machine, UserControl page) :
base(machine)
{
_page = page;
_page.LostFocus += Page_LostFocus;
_page.KeyDown += Page_KeyDown;
_page.KeyUp += Page_KeyUp;
_page.LostFocus += Page_LostFocus;
}
public override bool IsKeyDown(int key)
@ -57,15 +57,6 @@ private bool IsKeyDown(Key key)
return _states[(int)key];
}
private void Page_LostFocus(object sender, RoutedEventArgs e) // reset keyboard state on lost focus; can't access keyboard state on got focus
{
IsAnyKeyDown = false;
foreach (Key key in KeyValues)
{
_states[(int)key] = false;
}
}
private void Page_KeyDown(object sender, KeyEventArgs e)
{
_states[(int)e.Key] = true;
@ -86,6 +77,15 @@ private void Page_KeyUp(object sender, KeyEventArgs e)
_updateAnyKeyDown = true;
}
private void Page_LostFocus(object sender, RoutedEventArgs e) // reset keyboard state on lost focus; can't access keyboard state on got focus
{
IsAnyKeyDown = false;
foreach (Key key in KeyValues)
{
_states[(int)key] = false;
}
}
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
[SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode")]
private int GetAsciiKey(Key key, int platformKeyCode)

View File

@ -1,45 +1,14 @@
using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Jellyfish.Virtu.Services
{
public sealed class SilverlightStorageService : StorageService
{
public SilverlightStorageService(UserControl page)
public SilverlightStorageService(Machine machine) :
base(machine)
{
_dispatcher = page.Dispatcher;
}
public override string GetDiskFile()
{
string fileName = string.Empty;
// TODO
//ManualResetEvent syncEvent = new ManualResetEvent(false);
//DispatcherOperation operation = _dispatcher.BeginInvoke(() =>
//{
// try
// {
// OpenFileDialog dialog = new OpenFileDialog(); // SL expects all dialogs to be user initiated, ie from within an event handler.
// dialog.Filter = "Disk Files (*.dsk;*.nib)|*.dsk;*.nib|All Files (*.*)|*.*";
// bool? result = dialog.ShowDialog();
// if (result.HasValue && result.Value)
// {
// fileName = dialog.File.FullName;
// }
// }
// finally
// {
// syncEvent.Set();
// }
//});
//syncEvent.WaitOne();
return fileName;
}
public override void Load(string path, Action<Stream> reader)
@ -78,7 +47,5 @@ public override void Save(string path, Action<Stream> writer)
{
}
}
private Dispatcher _dispatcher;
}
}

View File

@ -3,21 +3,18 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Jellyfish.Virtu.Services
{
public sealed class SilverlightVideoService : VideoService
{
public SilverlightVideoService(Image image)
public SilverlightVideoService(Machine machine, Image image) :
base(machine)
{
_image = image;
SetImageSize();
_bitmap = new WriteableBitmap(BitmapWidth, BitmapHeight);
_pixels = new int[BitmapWidth * BitmapHeight];
_image.Source = _bitmap;
SetImageSize();
Application.Current.Host.Content.Resized += (sender, e) => SetImageSize();
}
@ -73,8 +70,8 @@ private void SetImageSize()
private const int BitmapHeight = 384;
private Image _image;
private WriteableBitmap _bitmap;
private int[] _pixels;
private WriteableBitmap _bitmap = new WriteableBitmap(BitmapWidth, BitmapHeight);
private int[] _pixels = new int[BitmapWidth * BitmapHeight];
private bool _pixelsDirty;
private bool _isFullScreen;
}

View File

@ -16,7 +16,7 @@ public override void Initialize()
public void ToggleOutput()
{
// TODO
_audioService.ToggleOutput();
}
private AudioService _audioService;

View File

@ -1,5 +1,4 @@
using System;
using Jellyfish.Virtu.Properties;
using Jellyfish.Virtu.Services;
using Jellyfish.Virtu.Settings;

View File

@ -42,10 +42,12 @@
</NoWarn>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=3.1.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d, processorArchitecture=x86" />
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
@ -128,6 +130,9 @@
<Compile Include="..\MachineEvents.cs">
<Link>Core\MachineEvents.cs</Link>
</Compile>
<Compile Include="..\Services\MachineService.cs">
<Link>Services\MachineService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineServices.cs">
<Link>Services\MachineServices.cs</Link>
</Compile>
@ -161,9 +166,13 @@
<Compile Include="..\Services\VideoService.cs">
<Link>Services\VideoService.cs</Link>
</Compile>
<Compile Include="..\Xna\Services\XnaGamePortService.cs">
<Link>Services\XnaGamePortService.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Services\WpfAudioService.cs" />
<Compile Include="Services\WpfKeyboardService.cs" />
<Compile Include="Services\WpfStorageService.cs" />
<Compile Include="Services\WpfVideoService.cs" />

View File

@ -1,9 +1,11 @@
using Jellyfish.Library;
using System.Security.Permissions;
using Jellyfish.Library;
namespace Jellyfish.Virtu
{
public partial class MainApp : ApplicationBase
public sealed partial class MainApp : ApplicationBase
{
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public MainApp() :
base("Virtu")
{

View File

@ -3,9 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:jl="clr-namespace:Jellyfish.Library;assembly=Jellyfish.Library"
Title="Virtu" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen">
<Grid Background="Black" Cursor="None">
<Image Name="_image" MinWidth="560" MinHeight="384" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
<jl:FrameRateCounter/>
<DockPanel Background="Black">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<Button x:Name="_disk1Button" Content="Disk 1" Focusable="false" IsTabStop="false" Margin="4 4 0 4"/>
<Button x:Name="_disk2Button" Content="Disk 2" Focusable="false" IsTabStop="false" Margin="4 4 0 4"/>
<jl:FrameRateCounter Margin="4 4 0 4" VerticalAlignment="Center"/>
</StackPanel>
<Grid Cursor="None">
<Image x:Name="_image" MinWidth="560" MinHeight="384" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Grid>
</DockPanel>
</Window>

View File

@ -1,58 +1,78 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using Jellyfish.Virtu.Services;
using Microsoft.Win32;
namespace Jellyfish.Virtu
{
public partial class MainWindow : Window
public sealed partial class MainWindow : Window, IDisposable
{
public MainWindow()
{
InitializeComponent();
_storageService = new WpfStorageService();
_keyboardService = new WpfKeyboardService(this);
_gamePortService = new GamePortService(); // not connected
_audioService = new AudioService(); // not connected
_videoService = new WpfVideoService(this, _image);
_storageService = new WpfStorageService(_machine);
_keyboardService = new WpfKeyboardService(_machine, this);
_gamePortService = new XnaGamePortService(_machine);
_audioService = new WpfAudioService(_machine, this);
_videoService = new WpfVideoService(_machine, this, _image);
_machine = new Machine();
_machine.Services.AddService(typeof(StorageService), _storageService);
_machine.Services.AddService(typeof(KeyboardService), _keyboardService);
_machine.Services.AddService(typeof(GamePortService), _gamePortService);
_machine.Services.AddService(typeof(AudioService), _audioService);
_machine.Services.AddService(typeof(VideoService), _videoService);
Loaded += MainWindow_Loaded;
Loaded += (sender, e) => _machine.Start();
CompositionTarget.Rendering += CompositionTarget_Rendering;
Application.Current.Exit += MainApp_Exit;
Application.Current.Exit += (sender, e) => _machine.Stop();
_disk1Button.Click += (sender, e) => DiskButton_Click(0);
_disk2Button.Click += (sender, e) => DiskButton_Click(1);
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
public void Dispose()
{
_machine.Start();
_machine.Dispose();
_storageService.Dispose();
_keyboardService.Dispose();
_gamePortService.Dispose();
_audioService.Dispose();
_videoService.Dispose();
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
_keyboardService.Update();
_gamePortService.Update();
_audioService.Update();
_videoService.Update();
}
private void MainApp_Exit(object sender, ExitEventArgs e)
private void DiskButton_Click(int drive)
{
_machine.Stop();
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "Disk Files (*.dsk;*.nib)|*.dsk;*.nib|All Files (*.*)|*.*";
bool? result = dialog.ShowDialog();
if (result.HasValue && result.Value)
{
using (FileStream stream = File.OpenRead(dialog.FileName))
{
_machine.Pause();
_machine.DiskII.Drives[drive].InsertDisk(dialog.FileName, stream, false);
_machine.Unpause();
}
}
}
private Machine _machine = new Machine();
private StorageService _storageService;
private KeyboardService _keyboardService;
private GamePortService _gamePortService;
private AudioService _audioService;
private VideoService _videoService;
private Machine _machine;
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using Jellyfish.Library;
namespace Jellyfish.Virtu.Services
{
public sealed class WpfAudioService : AudioService
{
public WpfAudioService(Machine machine, Window window) :
base(machine)
{
_window = window;
_window.SourceInitialized += (sender, e) => _directSound.Start(_window.GetHandle());
_directSound.Update += DirectSound_Update;
_window.Closed += (sender, e) => _directSound.Stop();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_directSound.Dispose();
}
}
private void DirectSound_Update(object sender, DirectSoundUpdateEventArgs e)
{
IntPtr buffer = e.Buffer;
Update(e.BufferSize, (source, count) =>
{
Marshal.Copy(source, 0, buffer, count);
buffer = (IntPtr)((long)buffer + count);
});
}
private Window _window;
private DirectSound _directSound = new DirectSound(SampleRate, SampleChannels, SampleBits, SampleSize);
}
}

View File

@ -8,13 +8,14 @@ namespace Jellyfish.Virtu.Services
{
public sealed class WpfKeyboardService : KeyboardService
{
public WpfKeyboardService(Window window)
public WpfKeyboardService(Machine machine, Window window) :
base(machine)
{
_window = window;
_window.GotKeyboardFocus += Window_GotKeyboardFocus;
_window.KeyDown += Window_KeyDown;
_window.KeyUp += Window_KeyUp;
_window.GotKeyboardFocus += (sender, e) => _updateAnyKeyDown = true;
}
public override bool IsKeyDown(int key)
@ -55,11 +56,6 @@ private bool IsKeyDown(Key key)
return _states[(int)key];
}
private void Window_GotKeyboardFocus(object sender, EventArgs e)
{
_updateAnyKeyDown = true;
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
_states[(int)((e.Key == Key.System) ? e.SystemKey : e.Key)] = true;

View File

@ -2,29 +2,14 @@
using System.Deployment.Application;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using Microsoft.Win32;
namespace Jellyfish.Virtu.Services
{
public sealed class WpfStorageService : StorageService
{
public override string GetDiskFile()
public WpfStorageService(Machine machine) :
base(machine)
{
string fileName = string.Empty;
Application.Current.Dispatcher.Invoke(new Action(() =>
{
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "Disk Files (*.dsk;*.nib)|*.dsk;*.nib|All Files (*.*)|*.*";
bool? result = dialog.ShowDialog();
if (result.HasValue && result.Value)
{
fileName = dialog.FileName;
}
}));
return fileName;
}
public override void Load(string path, Action<Stream> reader)

View File

@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Security.Permissions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -10,15 +11,14 @@ namespace Jellyfish.Virtu.Services
{
public sealed class WpfVideoService : VideoService
{
public WpfVideoService(Window window, Image image)
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public WpfVideoService(Machine machine, Window window, Image image) :
base(machine)
{
_window = window;
_image = image;
SetImageSize();
_bitmap = new WriteableBitmap(BitmapWidth, BitmapHeight, BitmapDpi, BitmapDpi, BitmapPixelFormat, null);
_pixels = new uint[BitmapWidth * BitmapHeight];
_image.Source = _bitmap;
SetImageSize();
SystemEvents.DisplaySettingsChanged += (sender, e) => SetImageSize();
}
@ -74,8 +74,8 @@ private void SetImageSize()
private Window _window;
private Image _image;
private WriteableBitmap _bitmap;
private uint[] _pixels;
private WriteableBitmap _bitmap = new WriteableBitmap(BitmapWidth, BitmapHeight, BitmapDpi, BitmapDpi, BitmapPixelFormat, null);
private uint[] _pixels = new uint[BitmapWidth * BitmapHeight];
private bool _pixelsDirty;
private bool _isFullScreen;
}

View File

@ -128,6 +128,9 @@
<Compile Include="..\Services\KeyboardService.cs">
<Link>Services\KeyboardService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineService.cs">
<Link>Services\MachineService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineServices.cs">
<Link>Services\MachineServices.cs</Link>
</Compile>
@ -149,6 +152,7 @@
<Compile Include="MainApp.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="MainGame.cs" />
<Compile Include="Services\XnaAudioService.cs" />
<Compile Include="Services\XnaGamePortService.cs" />
<Compile Include="Services\XnaKeyboardService.cs" />
<Compile Include="Services\XnaStorageService.cs" />

View File

@ -149,6 +149,9 @@
<Compile Include="..\Services\KeyboardService.cs">
<Link>Services\KeyboardService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineService.cs">
<Link>Services\MachineService.cs</Link>
</Compile>
<Compile Include="..\Services\MachineServices.cs">
<Link>Services\MachineServices.cs</Link>
</Compile>
@ -170,6 +173,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="MainApp.cs" />
<Compile Include="MainGame.cs" />
<Compile Include="Services\XnaAudioService.cs" />
<Compile Include="Services\XnaGamePortService.cs" />
<Compile Include="Services\XnaKeyboardService.cs" />
<Compile Include="Services\XnaStorageService.cs" />

View File

@ -1,5 +1,3 @@
using System;
namespace Jellyfish.Virtu
{
static class MainApp

View File

@ -18,13 +18,16 @@ public MainGame() :
GraphicsDeviceManager.PreferredBackBufferWidth = 560;
GraphicsDeviceManager.PreferredBackBufferHeight = 384;
#endif
_storageService = new XnaStorageService(this);
_keyboardService = new XnaKeyboardService();
_gamePortService = new XnaGamePortService();
_audioService = new AudioService(); // not connected
_videoService = new XnaVideoService(this);
_storageService = new XnaStorageService(_machine, this);
_keyboardService = new XnaKeyboardService(_machine);
_gamePortService = new XnaGamePortService(_machine);
#if XBOX
_audioService = new AudioService(_machine); // not connected
#else
_audioService = new XnaAudioService(_machine, this);
#endif
_videoService = new XnaVideoService(_machine, this);
_machine = new Machine();
_machine.Services.AddService(typeof(StorageService), _storageService);
_machine.Services.AddService(typeof(KeyboardService), _keyboardService);
_machine.Services.AddService(typeof(GamePortService), _gamePortService);
@ -32,6 +35,20 @@ public MainGame() :
_machine.Services.AddService(typeof(VideoService), _videoService);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_machine.Dispose();
_storageService.Dispose();
_keyboardService.Dispose();
_gamePortService.Dispose();
_audioService.Dispose();
_videoService.Dispose();
}
base.Dispose(disposing);
}
protected override void BeginRun()
{
_machine.Start();
@ -41,7 +58,6 @@ protected override void Update(GameTime gameTime)
{
_keyboardService.Update();
_gamePortService.Update();
_audioService.Update();
base.Update(gameTime);
}
@ -57,12 +73,12 @@ protected override void EndRun()
_machine.Stop();
}
private Machine _machine = new Machine();
private StorageService _storageService;
private KeyboardService _keyboardService;
private GamePortService _gamePortService;
private AudioService _audioService;
private VideoService _videoService;
private Machine _machine;
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Runtime.InteropServices;
using Jellyfish.Library;
namespace Jellyfish.Virtu.Services
{
#if WINDOWS
public sealed class XnaAudioService : AudioService
{
public XnaAudioService(Machine machine, GameBase game) :
base(machine)
{
_game = game;
_directSound.Start(_game.Window.Handle);
_directSound.Update += DirectSound_Update;
_game.Exiting += (sender, e) => _directSound.Stop();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_directSound.Dispose();
}
}
private void DirectSound_Update(object sender, DirectSoundUpdateEventArgs e)
{
IntPtr buffer = e.Buffer;
Update(e.BufferSize, (source, count) =>
{
Marshal.Copy(source, 0, buffer, count);
buffer = (IntPtr)((long)buffer + count);
});
}
private GameBase _game;
private DirectSound _directSound = new DirectSound(SampleRate, SampleChannels, SampleBits, SampleSize);
}
#endif
}

View File

@ -1,11 +1,15 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace Jellyfish.Virtu.Services
{
public sealed class XnaGamePortService : GamePortService
{
public XnaGamePortService(Machine machine) :
base(machine)
{
}
public override void Update()
{
_lastState = _state;

View File

@ -8,17 +8,11 @@ namespace Jellyfish.Virtu.Services
{
public sealed class XnaKeyboardService : KeyboardService
{
public XnaKeyboardService() :
this(TimeSpan.FromMilliseconds(500).Ticks, TimeSpan.FromMilliseconds(32).Ticks)
public XnaKeyboardService(Machine machine) :
base(machine)
{
}
public XnaKeyboardService(long repeatDelay, long repeatSpeed)
{
_repeatDelay = repeatDelay;
_repeatSpeed = repeatSpeed;
}
public override bool IsKeyDown(int key)
{
return IsKeyDown((Keys)key);
@ -46,7 +40,7 @@ public override void Update()
_lastKey = key;
_lastTime = DateTime.UtcNow.Ticks;
_repeatTime = _repeatDelay;
_repeatTime = RepeatDelay;
#if XBOX
int asciiKey = GetAsciiKey(key, ref gamePadState);
#else
@ -71,7 +65,7 @@ public override void Update()
if (time - _lastTime >= _repeatTime)
{
_lastTime = time;
_repeatTime = _repeatSpeed;
_repeatTime = RepeatSpeed;
#if XBOX
int asciiKey = GetAsciiKey(_lastKey, ref gamePadState);
#else
@ -356,6 +350,8 @@ where field.IsLiteral
where (key != Keys.None) // filter Keys.None
select key).ToArray();
#endif
private static readonly long RepeatDelay = TimeSpan.FromMilliseconds(500).Ticks;
private static readonly long RepeatSpeed = TimeSpan.FromMilliseconds(32).Ticks;
private KeyboardState _state;
private KeyboardState _lastState;
@ -363,7 +359,5 @@ where field.IsLiteral
private Keys _lastKey;
private long _lastTime;
private long _repeatTime;
private long _repeatDelay;
private long _repeatSpeed;
}
}

View File

@ -8,15 +8,10 @@ namespace Jellyfish.Virtu.Services
{
public sealed class XnaStorageService : StorageService
{
public XnaStorageService(GameBase game)
public XnaStorageService(Machine machine, GameBase game) :
base(machine)
{
_game = game;
_storageDevice = new Lazy<StorageDevice>(() => Guide.EndShowStorageDeviceSelector(Guide.BeginShowStorageDeviceSelector(null, null)));
}
public override string GetDiskFile()
{
return string.Empty;
}
public override void Load(string path, Action<Stream> reader)
@ -48,6 +43,6 @@ public override void Save(string path, Action<Stream> writer)
}
private GameBase _game;
private Lazy<StorageDevice> _storageDevice;
private Lazy<StorageDevice> _storageDevice = new Lazy<StorageDevice>(() => Guide.EndShowStorageDeviceSelector(Guide.BeginShowStorageDeviceSelector(null, null)));
}
}

View File

@ -6,15 +6,16 @@
namespace Jellyfish.Virtu.Services
{
public sealed class XnaVideoService : VideoService, IDisposable
public sealed class XnaVideoService : VideoService
{
public XnaVideoService(GameBase game)
public XnaVideoService(Machine machine, GameBase game) :
base(machine)
{
_game = game;
_game.GraphicsDeviceManager.PreparingDeviceSettings += GraphicsDeviceManager_PreparingDeviceSettings;
_game.GraphicsDeviceService.DeviceCreated += GraphicsDeviceService_DeviceCreated;
_game.GraphicsDeviceService.DeviceReset += GraphicsDeviceService_DeviceReset;
_game.GraphicsDeviceService.DeviceReset += (sender, e) => SetTexturePosition();
}
[SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "y*560")]
@ -53,11 +54,14 @@ public override void Update()
_spriteBatch.End();
}
public void Dispose()
protected override void Dispose(bool disposing)
{
if (disposing)
{
_spriteBatch.Dispose();
_texture.Dispose();
}
}
private void GraphicsDeviceManager_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
{
@ -83,11 +87,6 @@ private void GraphicsDeviceService_DeviceCreated(object sender, EventArgs e)
SetTexturePosition();
}
private void GraphicsDeviceService_DeviceReset(object sender, EventArgs e)
{
SetTexturePosition();
}
private void SetTexturePosition()
{
_texturePosition.X = (_graphicsDevice.PresentationParameters.BackBufferWidth - TextureWidth * _textureScale) / 2f; // centered