First pass on video output.
- Added SDL video output. - Send VI interrupts when the currently scanned line hits the interrupt register's. - Added some program options related to video.master
parent
34dfe21c05
commit
58a454f112
|
@ -57,6 +57,11 @@
|
|||
<Compile Include="Program.cs" />
|
||||
<Compile Include="AssemblyReleaseStreamAttribute.cs" />
|
||||
<Compile Include="Updater.cs" />
|
||||
<Compile Include="Point.cs" />
|
||||
<Compile Include="SDL\Window.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="SDL\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'] <stream>: 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 <width>x<height> <mode = 'fullscreen', 'f', 'borderless', 'b', 'windowed', 'w'>: 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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -100,6 +100,9 @@
|
|||
<Compile Include="RCP\VI\RealityCoprocessor.VideoInterface.VerticalVideoRegister.cs" />
|
||||
<Compile Include="RCP\VI\RealityCoprocessor.VideoInterface.VerticalBurstRegister.cs" />
|
||||
<Compile Include="RCP\VI\RealityCoprocessor.VideoInterface.ScaleRegister.cs" />
|
||||
<Compile Include="IVideoOutput.cs" />
|
||||
<Compile Include="VideoFrame.cs" />
|
||||
<Compile Include="Switch.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="CPU\" />
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
namespace DotN64
|
||||
{
|
||||
using static RCP.RealityCoprocessor;
|
||||
|
||||
public interface IVideoOutput
|
||||
{
|
||||
#region Methods
|
||||
void Draw(VideoFrame frame, VideoInterface vi, RDRAM ram);
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -15,7 +15,16 @@
|
|||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
namespace DotN64
|
||||
{
|
||||
public enum Switch : byte
|
||||
{
|
||||
Off,
|
||||
On
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
namespace DotN64
|
||||
{
|
||||
using static RCP.RealityCoprocessor;
|
||||
|
||||
public struct VideoFrame : System.IEquatable<VideoFrame>
|
||||
{
|
||||
#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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue