diff --git a/DotN64.Desktop/DotN64.Desktop.csproj b/DotN64.Desktop/DotN64.Desktop.csproj
index 8675248..0d08e4f 100644
--- a/DotN64.Desktop/DotN64.Desktop.csproj
+++ b/DotN64.Desktop/DotN64.Desktop.csproj
@@ -57,6 +57,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/DotN64.Desktop/Point.cs b/DotN64.Desktop/Point.cs
new file mode 100644
index 0000000..5628034
--- /dev/null
+++ b/DotN64.Desktop/Point.cs
@@ -0,0 +1,17 @@
+namespace DotN64.Desktop
+{
+ internal struct Point
+ {
+ #region Fields
+ public int X, Y;
+ #endregion
+
+ #region Constructors
+ public Point(int x, int y)
+ {
+ X = x;
+ Y = y;
+ }
+ #endregion
+ }
+}
diff --git a/DotN64.Desktop/Program.cs b/DotN64.Desktop/Program.cs
index 0a5eb04..7613e63 100644
--- a/DotN64.Desktop/Program.cs
+++ b/DotN64.Desktop/Program.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Reflection;
using DotN64.Desktop;
+using System.Linq;
[assembly: AssemblyTitle(nameof(DotN64))]
[assembly: AssemblyDescription("Nintendo 64 emulator.")]
@@ -10,6 +11,7 @@ using DotN64.Desktop;
namespace DotN64.Desktop
{
using Diagnostics;
+ using SDL;
internal static class Program
{
@@ -81,6 +83,29 @@ namespace DotN64.Desktop
case "-h":
ShowHelp();
return;
+ case "--no-video":
+ options.NoVideo = true;
+ break;
+ case "--video":
+ case "-v":
+ var resolution = args[++i].Split('x').Select(v => int.Parse(v));
+ options.VideoResolution = new Point(resolution.First(), resolution.Last());
+
+ switch (args[++i])
+ {
+ case "fullscreen":
+ case "f":
+ options.FullScreenVideo = true;
+ break;
+ case "borderless":
+ case "b":
+ options.BorderlessWindow = true;
+ break;
+ case "windowed":
+ case "w":
+ break;
+ }
+ break;
default:
options.Cartridge = arg;
break;
@@ -126,23 +151,34 @@ namespace DotN64.Desktop
private static void Run(Options options)
{
var nintendo64 = new Nintendo64();
- Debugger debugger = null;
+ Window window = null;
+
+ if (options.UseDebugger)
+ nintendo64.Debugger = new Debugger(nintendo64);
+
+ if (!options.NoVideo)
+ {
+ window = new Window(nintendo64, size: options.VideoResolution)
+ {
+ IsFullScreen = options.FullScreenVideo,
+ IsBorderless = options.BorderlessWindow
+ };
+ nintendo64.VideoOutput = window;
+ }
if (options.BootROM != null)
nintendo64.PIF.BootROM = File.ReadAllBytes(options.BootROM);
- if (options.UseDebugger)
- debugger = new Debugger(nintendo64);
-
if (options.Cartridge != null)
+ {
nintendo64.Cartridge = Cartridge.FromFile(new FileInfo(options.Cartridge));
- nintendo64.PowerOn();
+ if (window != null)
+ window.Title += $" - {nintendo64.Cartridge.ImageName.Trim()}";
+ }
- if (debugger == null)
- nintendo64.Run();
- else
- debugger.Run(true);
+ nintendo64.PowerOn();
+ nintendo64.Run();
}
private static void ShowInfo()
@@ -172,6 +208,8 @@ namespace DotN64.Desktop
Console.WriteLine("\t\t[action = 'stream', 's'] : Downloads an update from the specified release stream.");
Console.WriteLine("\t-r, --repair: Repairs the installation by redownloading the full program.");
Console.WriteLine("\t-h, --help: Shows this help.");
+ Console.WriteLine("\t-v, --video x : Sets the window mode.");
+ Console.WriteLine("\t--no-video: Disables the video output.");
}
#endregion
@@ -179,8 +217,9 @@ namespace DotN64.Desktop
private struct Options
{
#region Fields
- public bool UseDebugger;
+ public bool UseDebugger, NoVideo, FullScreenVideo, BorderlessWindow;
public string BootROM, Cartridge;
+ public Point? VideoResolution;
#endregion
}
#endregion
diff --git a/DotN64.Desktop/SDL/Window.cs b/DotN64.Desktop/SDL/Window.cs
new file mode 100644
index 0000000..3d6255e
--- /dev/null
+++ b/DotN64.Desktop/SDL/Window.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Runtime.InteropServices;
+using static SDL2.SDL;
+
+namespace DotN64.Desktop.SDL
+{
+ using RCP;
+ using static RCP.RealityCoprocessor.VideoInterface;
+
+ internal class Window : IVideoOutput, IDisposable
+ {
+ #region Fields
+ private readonly IntPtr window;
+ private readonly Nintendo64 nintendo64;
+ private IntPtr renderer, texture;
+ private VideoFrame lastFrame;
+ private bool isDisposed;
+ #endregion
+
+ #region Properties
+ public string Title
+ {
+ get => SDL_GetWindowTitle(window);
+ set => SDL_SetWindowTitle(window, value);
+ }
+
+ public Point Position
+ {
+ get
+ {
+ var position = new Point();
+ SDL_GetWindowPosition(window, out position.X, out position.Y);
+
+ return position;
+ }
+ set => SDL_SetWindowPosition(window, value.X, value.Y);
+ }
+
+ public Point Size
+ {
+ get
+ {
+ var size = new Point();
+ SDL_GetWindowSize(window, out size.X, out size.Y);
+
+ return size;
+ }
+ set => SDL_SetWindowSize(window, value.X, value.Y);
+ }
+
+ public bool IsFullScreen
+ {
+ get => (SDL_GetWindowFlags(window) & (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN) != 0;
+ set => SDL_SetWindowFullscreen(window, value ? (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP : 0); // Could also go exclusive fullscreen with desktop bounds.
+ }
+
+ public bool IsBorderless
+ {
+ get => (SDL_GetWindowFlags(window) & (uint)SDL_WindowFlags.SDL_WINDOW_BORDERLESS) != 0;
+ set => SDL_SetWindowBordered(window, !value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE);
+ }
+ #endregion
+
+ #region Constructors
+ public Window(Nintendo64 nintendo64, string title = null, Point? position = null, Point? size = null)
+ {
+ this.nintendo64 = nintendo64;
+ position = position ?? new Point(SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+ size = size ?? new Point(640, 480);
+ window = SDL_CreateWindow(title ?? nameof(DotN64), position.Value.X, position.Value.Y, size.Value.X, size.Value.Y, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_RESIZABLE);
+ }
+
+ ~Window()
+ {
+ Dispose(false);
+ }
+
+ static Window()
+ {
+ SDL_Init(SDL_INIT_VIDEO);
+
+ AppDomain.CurrentDomain.ProcessExit += (s, e) => SDL_Quit();
+ }
+ #endregion
+
+ #region Methods
+ private void PollEvents()
+ {
+ while (SDL_PollEvent(out var sdlEvent) != 0)
+ {
+ switch (sdlEvent.type)
+ {
+ case SDL_EventType.SDL_QUIT:
+ nintendo64.PowerOff();
+ break;
+ case SDL_EventType.SDL_KEYDOWN:
+ switch (sdlEvent.key.keysym.sym)
+ {
+ case SDL_Keycode.SDLK_ESCAPE:
+ nintendo64.PowerOff();
+ break;
+ case SDL_Keycode.SDLK_PAUSE:
+ nintendo64.Debugger = nintendo64.Debugger ?? new Diagnostics.Debugger(nintendo64);
+ break;
+ case SDL_Keycode.SDLK_r: // TODO: Reset.
+ break;
+ case SDL_Keycode.SDLK_f:
+ IsFullScreen = !IsFullScreen;
+ break;
+ }
+ break;
+ }
+ }
+ }
+
+ private IntPtr CreateTexture(VideoFrame frame)
+ {
+ uint pixelFormat = 0;
+
+ switch (frame.Size)
+ {
+ case ControlRegister.PixelSize.RGBA5553:
+ pixelFormat = SDL_PIXELFORMAT_RGBA5551;
+ break;
+ case ControlRegister.PixelSize.RGBA8888:
+ pixelFormat = SDL_PIXELFORMAT_RGBA8888;
+ break;
+ }
+
+ return SDL_CreateTexture(renderer, pixelFormat, (int)SDL_TextureAccess.SDL_TEXTUREACCESS_STREAMING, frame.Width, frame.Height);
+ }
+
+ public void Draw(VideoFrame frame, RealityCoprocessor.VideoInterface vi, RDRAM ram)
+ {
+ PollEvents();
+
+ if (renderer == IntPtr.Zero)
+ renderer = SDL_CreateRenderer(window, SDL_GetWindowDisplayIndex(window), SDL_RendererFlags.SDL_RENDERER_PRESENTVSYNC);
+
+ if (frame.Size <= ControlRegister.PixelSize.Reserved) // Do nothing on Blank or Reserved frame.
+ {
+ // Might want to clear the screen.
+ SDL_RenderPresent(renderer);
+ return;
+ }
+
+ if (frame != lastFrame)
+ {
+ SDL_DestroyTexture(texture);
+ texture = CreateTexture(frame);
+ lastFrame = frame;
+ }
+
+ var textureRect = new SDL_Rect
+ {
+ w = frame.Width,
+ h = frame.Height
+ };
+ var rendererRect = new SDL_Rect
+ {
+ w = Size.X,
+ h = Size.Y
+ };
+
+ SDL_LockTexture(texture, ref textureRect, out var pixels, out var pitch);
+
+ // TODO: This should be moved to the VI itself, which would call VideoOutput methods instead.
+ for (vi.CurrentVerticalLine = 0; vi.CurrentVerticalLine < vi.VerticalSync; vi.CurrentVerticalLine++) // Sweep all the way down the screen.
+ {
+ if (vi.CurrentVerticalLine >= vi.VerticalVideo.ActiveVideoStart && vi.CurrentVerticalLine < vi.VerticalVideo.ActiveVideoEnd) // Only scan active lines.
+ {
+ var offset = pitch * (vi.CurrentVerticalLine - vi.VerticalVideo.ActiveVideoStart);
+
+ Marshal.Copy(ram.Memory, (int)vi.DRAMAddress + offset, pixels + offset, pitch);
+ }
+ }
+
+ SDL_UnlockTexture(texture);
+
+ SDL_RenderClear(renderer);
+ SDL_RenderCopy(renderer, texture, ref textureRect, ref rendererRect);
+
+ SDL_RenderPresent(renderer);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (isDisposed)
+ return;
+
+ SDL_DestroyWindow(window);
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroyTexture(texture);
+
+ isDisposed = true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+ }
+}
diff --git a/DotN64/DotN64.csproj b/DotN64/DotN64.csproj
index 66b1955..884235f 100644
--- a/DotN64/DotN64.csproj
+++ b/DotN64/DotN64.csproj
@@ -100,6 +100,9 @@
+
+
+
diff --git a/DotN64/IVideoOutput.cs b/DotN64/IVideoOutput.cs
new file mode 100644
index 0000000..468bdb4
--- /dev/null
+++ b/DotN64/IVideoOutput.cs
@@ -0,0 +1,11 @@
+namespace DotN64
+{
+ using static RCP.RealityCoprocessor;
+
+ public interface IVideoOutput
+ {
+ #region Methods
+ void Draw(VideoFrame frame, VideoInterface vi, RDRAM ram);
+ #endregion
+ }
+}
diff --git a/DotN64/Nintendo64.cs b/DotN64/Nintendo64.cs
index be8aaff..858da3d 100644
--- a/DotN64/Nintendo64.cs
+++ b/DotN64/Nintendo64.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
namespace DotN64
{
@@ -30,6 +31,12 @@ namespace DotN64
CartridgeSwapped?.Invoke(this, value);
}
}
+
+ public IVideoOutput VideoOutput { get; set; }
+
+ public Switch Power { get; private set; }
+
+ public Diagnostics.Debugger Debugger { get; set; }
#endregion
#region Events
@@ -54,16 +61,38 @@ namespace DotN64
#region Methods
public void PowerOn()
{
+ Power = Switch.On;
+
CPU.Reset();
PIF.Reset();
}
+ public void PowerOff() => Power = Switch.Off;
+
public void Run()
{
- while (true)
- {
- CPU.Cycle();
- }
+ if (VideoOutput != null)
+ Task.Run(() =>
+ {
+ while (Power == Switch.On && VideoOutput != null)
+ {
+ VideoOutput.Draw(new VideoFrame(RCP.VI), RCP.VI, RAM);
+ }
+ });
+
+ if (Debugger == null)
+ while (Power == Switch.On)
+ {
+ if (Debugger == null)
+ CPU.Cycle();
+ else
+ {
+ Debugger.Run(true);
+ Debugger = null;
+ }
+ }
+ else
+ Debugger.Run(true);
}
#endregion
}
diff --git a/DotN64/RCP/VI/RealityCoprocessor.VideoInterface.cs b/DotN64/RCP/VI/RealityCoprocessor.VideoInterface.cs
index e2b9fb7..da8cd47 100644
--- a/DotN64/RCP/VI/RealityCoprocessor.VideoInterface.cs
+++ b/DotN64/RCP/VI/RealityCoprocessor.VideoInterface.cs
@@ -15,7 +15,16 @@
///
/// Current half line, sampled once per line (the lsb of V_CURRENT is constant within a field, and in interlaced modes gives the field number - which is constant for non-interlaced modes).
///
- public ushort CurrentVerticalLine { get; set; }
+ private ushort currentVerticalLine;
+ public ushort CurrentVerticalLine
+ {
+ get => currentVerticalLine;
+ set
+ {
+ if ((currentVerticalLine = value) == VerticalInterrupt)
+ rcp.MI.Interrupt |= MIPSInterface.Interrupts.VI;
+ }
+ }
public ControlRegister Control { get; set; }
diff --git a/DotN64/Switch.cs b/DotN64/Switch.cs
new file mode 100644
index 0000000..24d5f76
--- /dev/null
+++ b/DotN64/Switch.cs
@@ -0,0 +1,8 @@
+namespace DotN64
+{
+ public enum Switch : byte
+ {
+ Off,
+ On
+ }
+}
diff --git a/DotN64/VideoFrame.cs b/DotN64/VideoFrame.cs
new file mode 100644
index 0000000..909aee7
--- /dev/null
+++ b/DotN64/VideoFrame.cs
@@ -0,0 +1,38 @@
+namespace DotN64
+{
+ using static RCP.RealityCoprocessor;
+
+ public struct VideoFrame : System.IEquatable
+ {
+ #region Properties
+ public ushort Width { get; set; }
+
+ public ushort Height { get; set; }
+
+ public VideoInterface.ControlRegister.PixelSize Size { get; set; }
+ #endregion
+
+ #region Constructors
+ public VideoFrame(VideoInterface vi)
+ {
+ Width = (ushort)((vi.HorizontalVideo.ActiveVideoEnd - vi.HorizontalVideo.ActiveVideoStart) * (float)vi.HorizontalScale.ScaleUpFactor / (1 << 10));
+ Height = (ushort)(((vi.VerticalVideo.ActiveVideoEnd - vi.VerticalVideo.ActiveVideoStart) >> 1) * (float)vi.VerticalScale.ScaleUpFactor / (1 << 10));
+ Size = vi.Control.Type;
+ }
+ #endregion
+
+ #region Methods
+ public bool Equals(VideoFrame other) => other.Width == Width && other.Height == Height && other.Size == Size;
+
+ public override bool Equals(object obj) => obj is VideoFrame && Equals((VideoFrame)obj);
+
+ public override int GetHashCode() => Width ^ Height ^ (byte)Size;
+ #endregion
+
+ #region Operators
+ public static bool operator ==(VideoFrame left, VideoFrame right) => left.Equals(right);
+
+ public static bool operator !=(VideoFrame left, VideoFrame right) => !left.Equals(right);
+ #endregion
+ }
+}