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 + } +}