diff --git a/Parsons.Steamdeck/Parsons.Steamdeck.csproj b/Parsons.Steamdeck/Parsons.Steamdeck.csproj new file mode 100644 index 0000000..cfa13ad --- /dev/null +++ b/Parsons.Steamdeck/Parsons.Steamdeck.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + + true + true + Size + + + + + + + + + + + diff --git a/Parsons.Steamdeck/Program.cs b/Parsons.Steamdeck/Program.cs new file mode 100644 index 0000000..20500f8 --- /dev/null +++ b/Parsons.Steamdeck/Program.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using System.Numerics; +using Core; +using Raylib_cs; + +namespace Parsons.SteamDeck +{ + class Program + { + static void Main(string[] args) + { + // 1. Initialize Cross-Platform OpenGL Window + // THE FIX: Use PascalCase C# enums + Raylib.SetConfigFlags(ConfigFlags.ResizableWindow); + Raylib.InitWindow(1280, 800, "Parsons Master System"); // Native Steam Deck Resolution + Raylib.InitAudioDevice(); + + // 2. Initialize Hardware-Agnostic Audio Stream (44.1kHz, 32-bit float, Mono) + AudioStream audioStream = Raylib.LoadAudioStream(44100, 32, 1); + Raylib.PlayAudioStream(audioStream); + + // 3. Boot the Core Emulator + SmsMachine machine = new SmsMachine(); + RaylibAudioPlayer audioPlayer = new RaylibAudioPlayer(); + machine.AudioProcessor.AudioDevice = audioPlayer; + + // Load a ROM (Passed via command line argument, e.g. ./Parsons.SteamDeck sonic.sms) + if (args.Length > 0 && File.Exists(args[0])) + { + machine.LoadCartridge(File.ReadAllBytes(args[0])); + bool isGG = args[0].EndsWith(".gg", StringComparison.OrdinalIgnoreCase); + machine.VideoProcessor.IsGameGear = isGG; + } + + // Lock the engine loop to 60 FPS natively + Raylib.SetTargetFPS(60); + + // 4. Prepare the Video Texture Pipeline + // THE FIX: Use Color.Black + Image img = Raylib.GenImageColor(256, 192, Color.Black); + Texture2D vdpTexture = Raylib.LoadTextureFromImage(img); + byte[] pixelBuffer = new byte[256 * 192 * 4]; // Fast RGBA translation array + + // --- THE MAIN GAME LOOP --- + while (!Raylib.WindowShouldClose()) + { + // A. OS-Agnostic Input Polling (Works on Keyboard AND Steam Deck Controller) + // THE FIX: PascalCase KeyboardKey and GamepadButton enums + byte pad = 0xFF; + if (Raylib.IsKeyDown(KeyboardKey.Up) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceUp)) pad &= 0xFE; + if (Raylib.IsKeyDown(KeyboardKey.Down) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceDown)) pad &= 0xFD; + if (Raylib.IsKeyDown(KeyboardKey.Left) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceLeft)) pad &= 0xFB; + if (Raylib.IsKeyDown(KeyboardKey.Right) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceRight)) pad &= 0xF7; + + // Steam Deck "A" Button is RightFaceDown. "B" Button is RightFaceRight. + if (Raylib.IsKeyDown(KeyboardKey.Z) || Raylib.IsGamepadButtonDown(0, GamepadButton.RightFaceDown)) pad &= 0xEF; // B1 / A Button + if (Raylib.IsKeyDown(KeyboardKey.X) || Raylib.IsGamepadButtonDown(0, GamepadButton.RightFaceRight)) pad &= 0xDF; // B2 / B Button + + machine.IoBus.Joypad1Keyboard = pad; + + // B. Execute exactly one frame of T-States + machine.RunFrame(); + + // C. Flush Audio Buffer to the Sound Card + if (Raylib.IsAudioStreamProcessed(audioStream)) + { + float[] samples = audioPlayer.GetQueuedSamples(); + if (samples.Length > 0) + { + unsafe + { + fixed (float* p = samples) + { + Raylib.UpdateAudioStream(audioStream, p, samples.Length); + } + } + } + } + + // D. Fast Pixel Translation (ARGB int -> RGBA bytes) + int[] frameBuffer = machine.VideoProcessor.FrameBuffer; + for (int i = 0; i < frameBuffer.Length; i++) + { + int argb = frameBuffer[i]; + pixelBuffer[i * 4 + 0] = (byte)((argb >> 16) & 0xFF); // R + pixelBuffer[i * 4 + 1] = (byte)((argb >> 8) & 0xFF); // G + pixelBuffer[i * 4 + 2] = (byte)(argb & 0xFF); // B + pixelBuffer[i * 4 + 3] = 255; // A + } + + // Push bytes directly to the GPU Texture + unsafe + { + fixed (byte* p = pixelBuffer) + { + Raylib.UpdateTexture(vdpTexture, p); + } + } + + // E. Render to Screen (Perfect Aspect Ratio Scaling) + Raylib.BeginDrawing(); + Raylib.ClearBackground(Color.Black); + + float scale = Math.Min((float)Raylib.GetScreenWidth() / 256, (float)Raylib.GetScreenHeight() / 192); + Rectangle sourceRec = new Rectangle(0, 0, 256, 192); + Rectangle destRec = new Rectangle( + (Raylib.GetScreenWidth() - (256 * scale)) * 0.5f, + (Raylib.GetScreenHeight() - (192 * scale)) * 0.5f, + 256 * scale, 192 * scale); + + // Draw the VDP Frame + Raylib.DrawTexturePro(vdpTexture, sourceRec, destRec, new Vector2(0, 0), 0.0f, Color.White); + + Raylib.EndDrawing(); + } + + // Clean up hardware resources on exit + Raylib.UnloadTexture(vdpTexture); + Raylib.UnloadAudioStream(audioStream); + Raylib.CloseAudioDevice(); + Raylib.CloseWindow(); + } + } +} \ No newline at end of file diff --git a/Parsons.Steamdeck/RaylibAudioPlayer.cs b/Parsons.Steamdeck/RaylibAudioPlayer.cs new file mode 100644 index 0000000..9eea779 --- /dev/null +++ b/Parsons.Steamdeck/RaylibAudioPlayer.cs @@ -0,0 +1,28 @@ +using Core.Interfaces; +using System.Collections.Concurrent; + +namespace Parsons.SteamDeck +{ + public class RaylibAudioPlayer : IAudioDevice + { + private ConcurrentQueue _sampleQueue = new ConcurrentQueue(); + + public void AddSample(float sample) + { + // Queue the sample from the emulator core + _sampleQueue.Enqueue(sample); + } + + public float[] GetQueuedSamples() + { + // Pull all pending samples out of the queue to send to the sound card + int count = _sampleQueue.Count; + float[] buffer = new float[count]; + for (int i = 0; i < count; i++) + { + _sampleQueue.TryDequeue(out buffer[i]); + } + return buffer; + } + } +} \ No newline at end of file diff --git a/ParsonsMasterSystem2026.sln b/ParsonsMasterSystem2026.sln index 8613677..c38a4ff 100644 --- a/ParsonsMasterSystem2026.sln +++ b/ParsonsMasterSystem2026.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.37216.2 d17.14 +VisualStudioVersion = 17.14.37216.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Desktop", "Desktop\Desktop.csproj", "{E245F3A5-E541-43BB-B64E-B4B2BB3D8C51}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{B0A741E4-CE3E-4DF0-96B2-E314973F9201}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parsons.Steamdeck", "Parsons.Steamdeck\Parsons.Steamdeck.csproj", "{B619DF68-7EBF-4C6A-AC24-AD5250404943}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {B0A741E4-CE3E-4DF0-96B2-E314973F9201}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0A741E4-CE3E-4DF0-96B2-E314973F9201}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0A741E4-CE3E-4DF0-96B2-E314973F9201}.Release|Any CPU.Build.0 = Release|Any CPU + {B619DF68-7EBF-4C6A-AC24-AD5250404943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B619DF68-7EBF-4C6A-AC24-AD5250404943}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B619DF68-7EBF-4C6A-AC24-AD5250404943}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B619DF68-7EBF-4C6A-AC24-AD5250404943}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE