Steam Deck version fixes

This commit is contained in:
2026-06-04 22:01:45 +01:00
parent 2300e2931e
commit ef365185bf
96 changed files with 198 additions and 73 deletions

View File

@@ -7,10 +7,14 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishAot>true</PublishAot>
<OptimizationPreference>Size</OptimizationPreference> <OptimizationPreference>Size</OptimizationPreference>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="ROMS\**\*.sms" />
<EmbeddedResource Include="ROMS\**\*.gg" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Raylib-cs" Version="8.0.0" /> <PackageReference Include="Raylib-cs" Version="8.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -10,61 +10,133 @@ namespace Parsons.SteamDeck
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
// 1. Initialize Cross-Platform OpenGL Window
// THE FIX: Use PascalCase C# enums
Raylib.SetConfigFlags(ConfigFlags.ResizableWindow); Raylib.SetConfigFlags(ConfigFlags.ResizableWindow);
Raylib.InitWindow(1280, 800, "Parsons Master System"); // Native Steam Deck Resolution Raylib.InitWindow(1280, 800, "Parsons Master System 2026");
Raylib.InitAudioDevice(); Raylib.InitAudioDevice();
// 2. Initialize Hardware-Agnostic Audio Stream (44.1kHz, 32-bit float, Mono) //Raylib.SetAudioStreamBufferSizeDefault(1024);
AudioStream audioStream = Raylib.LoadAudioStream(44100, 32, 1); AudioStream audioStream = Raylib.LoadAudioStream(44100, 32, 1);
Raylib.PlayAudioStream(audioStream); Raylib.PlayAudioStream(audioStream);
// 3. Boot the Core Emulator
SmsMachine machine = new SmsMachine(); SmsMachine machine = new SmsMachine();
RaylibAudioPlayer audioPlayer = new RaylibAudioPlayer(); RaylibAudioPlayer audioPlayer = new RaylibAudioPlayer();
machine.AudioProcessor.AudioDevice = audioPlayer; 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); Raylib.SetTargetFPS(60);
// 4. Prepare the Video Texture Pipeline
// THE FIX: Use Color.Black
Image img = Raylib.GenImageColor(256, 192, Color.Black); Image img = Raylib.GenImageColor(256, 192, Color.Black);
Texture2D vdpTexture = Raylib.LoadTextureFromImage(img); Texture2D vdpTexture = Raylib.LoadTextureFromImage(img);
byte[] pixelBuffer = new byte[256 * 192 * 4]; // Fast RGBA translation array byte[] pixelBuffer = new byte[256 * 192 * 4];
bool isMenuState = true;
string[] embeddedRoms = GetEmbeddedRoms();
int selectedRomIndex = 0;
bool buttonLock = false;
// --- THE MAIN GAME LOOP ---
while (!Raylib.WindowShouldClose()) while (!Raylib.WindowShouldClose())
{ {
// A. OS-Agnostic Input Polling (Works on Keyboard AND Steam Deck Controller) if (isMenuState)
// THE FIX: PascalCase KeyboardKey and GamepadButton enums {
// --- MENU INPUT LOGIC ---
if (!buttonLock)
{
if (Raylib.IsKeyDown(KeyboardKey.S) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceDown))
{
selectedRomIndex++;
if (selectedRomIndex >= embeddedRoms.Length) selectedRomIndex = 0;
buttonLock = true;
}
else if (Raylib.IsKeyDown(KeyboardKey.W) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceUp))
{
selectedRomIndex--;
if (selectedRomIndex < 0) selectedRomIndex = embeddedRoms.Length - 1;
buttonLock = true;
}
else if (Raylib.IsKeyDown(KeyboardKey.O) || Raylib.IsGamepadButtonDown(0, GamepadButton.RightFaceDown))
{
string targetRom = embeddedRoms[selectedRomIndex];
machine.LoadCartridge(LoadEmbeddedRomBytes(targetRom));
machine.VideoProcessor.IsGameGear = targetRom.EndsWith(".gg", StringComparison.OrdinalIgnoreCase);
isMenuState = false;
}
}
if (Raylib.IsKeyUp(KeyboardKey.S) && Raylib.IsKeyUp(KeyboardKey.W) &&
Raylib.IsGamepadButtonUp(0, GamepadButton.LeftFaceDown) && Raylib.IsGamepadButtonUp(0, GamepadButton.LeftFaceUp))
{
buttonLock = false;
}
// --- SCROLLING MENU DRAW LOGIC ---
Raylib.BeginDrawing();
Raylib.ClearBackground(new Color(20, 20, 40, 255));
Raylib.DrawText("PARSONS MASTER SYSTEM", 50, 50, 40, Color.White);
Raylib.DrawText("Select: D-Pad Start: 'A' Button Return to Menu: Select + Start", 50, 100, 20, Color.LightGray);
// THE FIX: Sliding Window Camera Math
int maxVisibleItems = 16;
int startIndex = selectedRomIndex - (maxVisibleItems / 2);
if (startIndex < 0) startIndex = 0;
if (startIndex > embeddedRoms.Length - maxVisibleItems) startIndex = Math.Max(0, embeddedRoms.Length - maxVisibleItems);
for (int i = 0; i < maxVisibleItems && (startIndex + i) < embeddedRoms.Length; i++)
{
int actualIndex = startIndex + i;
string cleanName = embeddedRoms[actualIndex].Replace("Parsons.Steamdeck.ROMS.", "");
Color textColor = (actualIndex == selectedRomIndex) ? Color.Yellow : Color.White;
string prefix = (actualIndex == selectedRomIndex) ? ">> " : " ";
Raylib.DrawText(prefix + cleanName, 80, 160 + (i * 35), 20, textColor);
}
Raylib.EndDrawing();
}
else
{
// --- EMULATOR HOTKEYS ---
// If the user holds the "Select" button (or Shift on Keyboard)
if (Raylib.IsGamepadButtonDown(0, GamepadButton.MiddleLeft) || Raylib.IsKeyDown(KeyboardKey.LeftShift))
{
// Select + Start = Return to Menu
if (Raylib.IsGamepadButtonPressed(0, GamepadButton.MiddleRight) || Raylib.IsKeyPressed(KeyboardKey.Enter))
{
isMenuState = true;
machine.Reset();
}
// Select + R1 = Save State
if (Raylib.IsGamepadButtonPressed(0, GamepadButton.RightTrigger1) || Raylib.IsKeyPressed(KeyboardKey.F5))
{
machine.SaveState("steamdeck.state");
}
// Select + L1 = Load State
if (Raylib.IsGamepadButtonPressed(0, GamepadButton.LeftTrigger1) || Raylib.IsKeyPressed(KeyboardKey.F8))
{
machine.LoadState("steamdeck.state");
}
}
// --- EMULATOR LOGIC ---
byte pad = 0xFF; byte pad = 0xFF;
if (Raylib.IsKeyDown(KeyboardKey.Up) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceUp)) pad &= 0xFE; 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.Down) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceDown)) pad &= 0xFD;
if (Raylib.IsKeyDown(KeyboardKey.Left) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceLeft)) pad &= 0xFB; if (Raylib.IsKeyDown(KeyboardKey.Left) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceLeft)) pad &= 0xFB;
if (Raylib.IsKeyDown(KeyboardKey.Right) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceRight)) pad &= 0xF7; if (Raylib.IsKeyDown(KeyboardKey.Right) || Raylib.IsGamepadButtonDown(0, GamepadButton.LeftFaceRight)) pad &= 0xF7;
if (Raylib.IsKeyDown(KeyboardKey.O) || Raylib.IsGamepadButtonDown(0, GamepadButton.RightFaceDown)) pad &= 0xEF;
// Steam Deck "A" Button is RightFaceDown. "B" Button is RightFaceRight. if (Raylib.IsKeyDown(KeyboardKey.P) || Raylib.IsGamepadButtonDown(0, GamepadButton.RightFaceRight)) pad &= 0xDF;
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; machine.IoBus.Joypad1Keyboard = pad;
// B. Execute exactly one frame of T-States
machine.RunFrame(); machine.RunFrame();
// C. Flush Audio Buffer to the Sound Card // C. Flush Audio Buffer to the Sound Card
if (Raylib.IsAudioStreamProcessed(audioStream)) // THE FIX: Do not wait for Raylib to ask! Shove the exact number of
{ // samples we generated this frame directly into the ring buffer.
float[] samples = audioPlayer.GetQueuedSamples(); float[] samples = audioPlayer.GetQueuedSamples();
if (samples.Length > 0) if (samples.Length > 0)
{ {
@@ -76,20 +148,32 @@ namespace Parsons.SteamDeck
} }
} }
} }
}
// D. Fast Pixel Translation (ARGB int -> RGBA bytes) //if (Raylib.IsAudioStreamProcessed(audioStream))
//{
// if (audioPlayer.AvailableSamples >= 1024)
// {
// float[] samples = audioPlayer.GetSamples(1024);
// unsafe
// {
// fixed (float* p = samples)
// {
// Raylib.UpdateAudioStream(audioStream, p, 1024);
// }
// }
// }
//}
int[] frameBuffer = machine.VideoProcessor.FrameBuffer; int[] frameBuffer = machine.VideoProcessor.FrameBuffer;
for (int i = 0; i < frameBuffer.Length; i++) for (int i = 0; i < frameBuffer.Length; i++)
{ {
int argb = frameBuffer[i]; int argb = frameBuffer[i];
pixelBuffer[i * 4 + 0] = (byte)((argb >> 16) & 0xFF); // R pixelBuffer[i * 4 + 0] = (byte)((argb >> 16) & 0xFF);
pixelBuffer[i * 4 + 1] = (byte)((argb >> 8) & 0xFF); // G pixelBuffer[i * 4 + 1] = (byte)((argb >> 8) & 0xFF);
pixelBuffer[i * 4 + 2] = (byte)(argb & 0xFF); // B pixelBuffer[i * 4 + 2] = (byte)(argb & 0xFF);
pixelBuffer[i * 4 + 3] = 255; // A pixelBuffer[i * 4 + 3] = 255;
} }
// Push bytes directly to the GPU Texture
unsafe unsafe
{ {
fixed (byte* p = pixelBuffer) fixed (byte* p = pixelBuffer)
@@ -98,7 +182,6 @@ namespace Parsons.SteamDeck
} }
} }
// E. Render to Screen (Perfect Aspect Ratio Scaling)
Raylib.BeginDrawing(); Raylib.BeginDrawing();
Raylib.ClearBackground(Color.Black); Raylib.ClearBackground(Color.Black);
@@ -109,17 +192,36 @@ namespace Parsons.SteamDeck
(Raylib.GetScreenHeight() - (192 * scale)) * 0.5f, (Raylib.GetScreenHeight() - (192 * scale)) * 0.5f,
256 * scale, 192 * scale); 256 * scale, 192 * scale);
// Draw the VDP Frame
Raylib.DrawTexturePro(vdpTexture, sourceRec, destRec, new Vector2(0, 0), 0.0f, Color.White); Raylib.DrawTexturePro(vdpTexture, sourceRec, destRec, new Vector2(0, 0), 0.0f, Color.White);
Raylib.EndDrawing(); Raylib.EndDrawing();
} }
}
// Clean up hardware resources on exit
Raylib.UnloadTexture(vdpTexture); Raylib.UnloadTexture(vdpTexture);
Raylib.UnloadAudioStream(audioStream); Raylib.UnloadAudioStream(audioStream);
Raylib.CloseAudioDevice(); Raylib.CloseAudioDevice();
Raylib.CloseWindow(); Raylib.CloseWindow();
} }
private static string[] GetEmbeddedRoms()
{
// Pulls all embedded files and filters for Master System and Game Gear
return System.Reflection.Assembly.GetExecutingAssembly()
.GetManifestResourceNames()
.Where(r => r.EndsWith(".sms", StringComparison.OrdinalIgnoreCase) ||
r.EndsWith(".gg", StringComparison.OrdinalIgnoreCase))
.ToArray();
}
private static byte[] LoadEmbeddedRomBytes(string resourceName)
{
using (Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
using (MemoryStream ms = new MemoryStream())
{
stream.CopyTo(ms);
return ms.ToArray();
}
}
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -7,15 +7,24 @@ namespace Parsons.SteamDeck
{ {
private ConcurrentQueue<float> _sampleQueue = new ConcurrentQueue<float>(); private ConcurrentQueue<float> _sampleQueue = new ConcurrentQueue<float>();
// 4096 samples provides a perfect ~90ms safety buffer
private const int MaxBufferSamples = 4096;
public void AddSample(float sample) public void AddSample(float sample)
{ {
// Queue the sample from the emulator core
_sampleQueue.Enqueue(sample); _sampleQueue.Enqueue(sample);
// THE FIX: If the CPU outpaces the soundcard, drop the OLDEST sample!
// This guarantees we never fall behind and the audio stays perfectly synced.
if (_sampleQueue.Count > MaxBufferSamples)
{
_sampleQueue.TryDequeue(out _);
}
} }
// Grab absolutely everything we generated this frame
public float[] GetQueuedSamples() public float[] GetQueuedSamples()
{ {
// Pull all pending samples out of the queue to send to the sound card
int count = _sampleQueue.Count; int count = _sampleQueue.Count;
float[] buffer = new float[count]; float[] buffer = new float[count];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)