Compare commits

..

6 Commits

Author SHA1 Message Date
GreemDev
0a6a95754c Attempt 1 2026-05-12 20:01:40 -05:00
Renovate Bot
49891ba7af Update avalonia monorepo to 11.3.15 (#85)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Avalonia](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia/11.3.14/11.3.15?slim=true) |
| [Avalonia.Desktop](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Desktop/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Desktop/11.3.14/11.3.15?slim=true) |
| [Avalonia.Diagnostics](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Diagnostics/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Diagnostics/11.3.14/11.3.15?slim=true) |
| [Avalonia.Markup.Xaml.Loader](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Markup.Xaml.Loader/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Markup.Xaml.Loader/11.3.14/11.3.15?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/85
2026-05-12 02:41:10 +00:00
Max
89ea41ef84 Check if the Device is rendering before waiting on it (#86)
Fixes an issue where there were missed references and an ``OperationCancelled`` exception when exiting an application.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/86
2026-05-12 02:38:09 +00:00
yell0wsuit
f8167eb625 [HLE] Match hardware screenshot buffer size behavior for captures (#44)
## Description

~~Fixes a fatal CLR crash when `caps` screenshot saving receives an input buffer larger than `0x384000`.~~

~~Resolves a crash in Tomodachi Life: Living the Dream where saving the pictures to the system's album crashes with 0x80131506.~~

Follow up to #18. This PR adjusts the validation and copy behavior to better match real hardware, and adds logging to make invalid screenshot buffer cases easier to diagnose.

Real hardware accepts screenshot buffers with a size greater than or equal to `0x384000`, but only `0x384000` bytes are needed for the 1280x720 RGBA image.

This changes screenshot saving to:

- reject buffers smaller than `0x384000`
- accept buffers equal to or larger than `0x384000`
- copy only the first `0x384000` bytes into the 1280x720 bitmap

## Testing

Tested with a real Switch NRO using `capssuSaveScreenShotEx0`, `capssuSaveScreenShotEx1`, and `capssuSaveScreenShotEx2`.

Observed hardware behavior:

```text
0x384000     => OK
0x384000 - 1 => NullInputBuffer
0x384000 + 1 => OK
0x3C0000     => OK // Tomo life picture size
```

Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/44
2026-05-10 23:57:05 +00:00
Babib3l
bab160d650 Fix Windows fullscreen gap when toggling from maximized (#80)
This PR aims to Fix the Windows fullscreen gap when toggling from maximized state by using canonical Win32 fullscreen approach. When entering fullscreen, the window style is saved and replaced with WS_POPUP | WS_VISIBLE to remove all window chrome (title bar, borders), then SetWindowPos sizes the window to cover the full screen with SWP_FRAMECHANGED to force Win32 to recalculate the frame. On exit, the original window style is restored. This eliminates the few-pixel gap at the top that occurred because Avalonia's WindowState = FullScreen transition from a maximized state retained the title bar non-client area, leaving visible space at the top of the screen (Fullscreen resolution would be 1920 x 1072p on a 1080p monitor, it now correctly renders at 1920 x 1080p)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/80
2026-05-10 23:47:39 +00:00
yell0wsuit
e8cc252d9a [HLE] Fix StoreData layout and implement IDatabaseService.Append (#43)
- Fixes `StoreData` layout/update handling so `UpdateLatest` returns the stored Mii data correctly.
- Implements `IDatabaseService.Append` (https://switchbrew.org/wiki/Shared_Database_services#IDatabaseService)

Also adds regression tests for `UpdateLatest` and `Append`.

(Might) fix Mario Kart 8 Deluxe crashing on first boot due to failed Mii verification check (due to custom Mii from emulator), and potentially Tomodachi Life: Living the Dream for the "Import Mii from system" option.

Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/43
2026-05-10 23:39:19 +00:00
16 changed files with 657 additions and 239 deletions

View File

@@ -21,7 +21,7 @@ jobs:
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
image: ghcr.io/gruke-build/ubuntu:dotnet-latest
timeout-minutes: 45
strategy:
matrix:
@@ -37,19 +37,6 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install 7zip
run: |
sudo apt update && sudo apt install -y 7zip
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.forgejo/csc.json"
@@ -107,14 +94,6 @@ jobs:
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
@@ -141,7 +120,9 @@ jobs:
build_macos:
name: macOS Universal (${{ matrix.configuration }})
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/gruke-build/ubuntu:dotnet-latest
timeout-minutes: 45
strategy:
matrix:
@@ -150,21 +131,12 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install rcodesign
run: |
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz

View File

@@ -31,29 +31,16 @@ jobs:
strategy:
matrix:
platform:
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
- { name: win-x64, os: ghcr.io/gruke-build/ubuntu:dotnet-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/gruke-build/ubuntu:dotnet-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/gruke-build/ubuntu:dotnet-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/gruke-build/ubuntu:dotnet-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.forgejo/csc.json"
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install 7zip
run: |
sudo apt update && sudo apt install -y 7zip
- name: Get version info
id: version_info
run: |
@@ -105,14 +92,6 @@ jobs:
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
@@ -149,24 +128,15 @@ jobs:
name: Release MacOS universal
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
image: ghcr.io/gruke-build/ubuntu:dotnet-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install rcodesign
run: |
@@ -212,18 +182,11 @@ jobs:
name: Post CI Steps
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
image: ghcr.io/gruke-build/ubuntu:act-latest
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v6
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Get version info
id: version_info
run: |

View File

@@ -25,28 +25,15 @@ jobs:
strategy:
matrix:
platform:
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
- { name: win-x64, os: ghcr.io/gruke-build/ubuntu:act-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/gruke-build/ubuntu:act-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/gruke-build/ubuntu:act-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/gruke-build/ubuntu:act-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
- name: Install 7zip
run: |
sudo apt install -y 7zip
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Get version info
id: version_info
@@ -102,14 +89,6 @@ jobs:
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt install -y zsync desktop-file-utils appstream
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
@@ -143,7 +122,9 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/gruke-build/ubuntu:dotnet-latest
steps:
- uses: actions/checkout@v6
@@ -156,11 +137,6 @@ jobs:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install rcodesign
run: |
@@ -205,18 +181,13 @@ jobs:
post_ci:
name: Post-CI Steps
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/gruke-build/ubuntu:act-latest
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v6
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Get version info
id: version_info
run: |

View File

@@ -3,11 +3,11 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.14" />
<PackageVersion Include="Avalonia" Version="11.3.15" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.14" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.14" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.14" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.15" />
<PackageVersion Include="SharpCompress" Version="0.48.0" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.5" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.5" />

View File

@@ -176,9 +176,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
}
// This can somehow become -1.
// Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}");
Debug.Assert(_referenceCount >= 0);
}

View File

@@ -1,7 +1,9 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SkiaSharp;
using System;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -9,16 +11,20 @@ using System.Security.Cryptography;
namespace Ryujinx.HLE.HOS.Services.Caps
{
class CaptureManager
internal class CaptureManager
{
private readonly string _sdCardPath;
public CaptureManager(Switch device)
{
_ = device;
}
private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
private uint _shimLibraryVersion;
public CaptureManager(Switch device)
{
_sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
}
private const int ScreenshotWidth = 1280;
private const int ScreenshotHeight = 720;
private const int ScreenshotBytesPerPixel = 4;
private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * ScreenshotBytesPerPixel; // 0x384000
public ResultCode SetShimLibraryVersion(ServiceCtx context)
{
@@ -53,84 +59,94 @@ namespace Ryujinx.HLE.HOS.Services.Caps
return resultCode;
}
public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry)
public ResultCode SaveScreenShot(
byte[] screenshotData,
ulong appletResourceUserId,
ulong titleId,
out ApplicationAlbumEntry applicationAlbumEntry)
{
Logger.Stub?.PrintStub(LogClass.ServiceCaps, new
{
appletResourceUserId,
titleId,
screenshotDataLength = screenshotData?.Length ?? 0,
});
applicationAlbumEntry = default;
if (screenshotData.Length == 0)
if (screenshotData == null || screenshotData.Length == 0)
{
return ResultCode.NullInputBuffer;
}
/*
// NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now.
if (appletResourceUserId == 0)
if (screenshotData.Length < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotData.Length:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
DateTime currentDateTime = DateTime.Now;
applicationAlbumEntry = new ApplicationAlbumEntry()
{
Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(),
TitleId = titleId,
AlbumFileDateTime = new AlbumFileDateTime()
{
Year = (ushort)currentDateTime.Year,
Month = (byte)currentDateTime.Month,
Day = (byte)currentDateTime.Day,
Hour = (byte)currentDateTime.Hour,
Minute = (byte)currentDateTime.Minute,
Second = (byte)currentDateTime.Second,
UniqueId = 0,
},
AlbumStorage = AlbumStorage.Sd,
ContentType = ContentType.Screenshot,
Padding = new Array5<byte>(),
Unknown0x1f = 1,
};
// NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead.
string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20];
string folderPath = Path.Combine(
_sdCardPath,
"Nintendo",
"Album",
currentDateTime.Year.ToString("0000", CultureInfo.InvariantCulture),
currentDateTime.Month.ToString("00", CultureInfo.InvariantCulture),
currentDateTime.Day.ToString("00", CultureInfo.InvariantCulture));
string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
_ = Directory.CreateDirectory(folderPath);
while (File.Exists(filePath))
{
applicationAlbumEntry.AlbumFileDateTime.UniqueId++;
filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
}
using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888));
IntPtr pixels = bitmap.GetPixels();
if (pixels == IntPtr.Zero)
{
return ResultCode.InvalidArgument;
}
*/
/*
// Doesn't occur in our case.
if (applicationAlbumEntry == null)
{
return ResultCode.NullOutputBuffer;
}
*/
Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize);
if (screenshotData.Length >= 0x384000)
{
DateTime currentDateTime = DateTime.Now;
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);
applicationAlbumEntry = new ApplicationAlbumEntry()
{
Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(),
TitleId = titleId,
AlbumFileDateTime = new AlbumFileDateTime()
{
Year = (ushort)currentDateTime.Year,
Month = (byte)currentDateTime.Month,
Day = (byte)currentDateTime.Day,
Hour = (byte)currentDateTime.Hour,
Minute = (byte)currentDateTime.Minute,
Second = (byte)currentDateTime.Second,
UniqueId = 0,
},
AlbumStorage = AlbumStorage.Sd,
ContentType = ContentType.Screenshot,
Padding = new Array5<byte>(),
Unknown0x1f = 1,
};
// NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead.
string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20];
string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00"));
string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
// TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions.
Directory.CreateDirectory(folderPath);
while (File.Exists(filePath))
{
applicationAlbumEntry.AlbumFileDateTime.UniqueId++;
filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
}
// NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data.
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul));
int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length;
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen);
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);
return ResultCode.Success;
}
return ResultCode.NullInputBuffer;
return ResultCode.Success;
}
private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash)

View File

@@ -1,13 +1,19 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Caps.Types;
namespace Ryujinx.HLE.HOS.Services.Caps
{
[Service("caps:su")] // 6.0.0+
class IScreenShotApplicationService : IpcService
internal class IScreenShotApplicationService : IpcService
{
public IScreenShotApplicationService(ServiceCtx context) { }
private const ulong ScreenshotDataSize = 0x384000;
private const ulong ApplicationDataSize = 0x404;
public IScreenShotApplicationService(ServiceCtx context)
{
_ = context;
}
[CommandCmif(32)] // 7.0.0+
// SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId)
public ResultCode SetShimLibraryVersion(ServiceCtx context)
@@ -33,6 +39,15 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[0].Position;
ulong screenshotDataSize = context.Request.SendBuff[0].Size;
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
@@ -60,6 +75,24 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[1].Position;
ulong screenshotDataSize = context.Request.SendBuff[1].Size;
if (applicationDataSize != ApplicationDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid ApplicationData size 0x{applicationDataSize:X}; expected 0x{ApplicationDataSize:X}.");
return ResultCode.InvalidArgument;
}
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
// TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now).
_ = context.Memory.GetSpan(applicationDataPosition, (int)applicationDataSize).ToArray();
@@ -88,6 +121,23 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[1].Position;
ulong screenshotDataSize = context.Request.SendBuff[1].Size;
if (userIdListSize != 0x88)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid UserIdList size 0x{userIdListSize:X}; expected 0x88.");
return ResultCode.InvalidArgument;
}
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
// TODO: Parse the UserIdList.
_ = context.Memory.GetSpan(userIdListPosition, (int)userIdListSize).ToArray();

View File

@@ -81,8 +81,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return ResultCode.Success;
}
public ResultCode UpdateLatest<T>(DatabaseSessionMetadata metadata, IStoredData<T> oldMiiData, SourceFlag flag, IStoredData<T> newMiiData) where T : unmanaged
public ResultCode UpdateLatest<T>(DatabaseSessionMetadata metadata, T oldMiiData, SourceFlag flag, out T newMiiData) where T : unmanaged, IStoredData<T>
{
newMiiData = default;
if (!flag.HasFlag(SourceFlag.Database))
{
return ResultCode.NotFound;
@@ -106,7 +108,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
newMiiData.SetFromStoreData(storeData);
if (oldMiiData == newMiiData)
if (oldMiiData.Equals(newMiiData))
{
return ResultCode.NotUpdated;
}
@@ -286,6 +288,18 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return result;
}
public ResultCode Append(DatabaseSessionMetadata metadata, CharInfo charInfo)
{
ResultCode result = _miiDatabase.Append(metadata, _utilityImpl, charInfo);
if (result == ResultCode.Success)
{
result = _miiDatabase.SaveDatabase();
}
return result;
}
public ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData)
{
coreData = new CoreData();

View File

@@ -449,6 +449,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return ResultCode.Success;
}
public ResultCode Append(DatabaseSessionMetadata metadata, UtilityImpl utilityImpl, CharInfo charInfo)
{
if (!charInfo.IsValid())
{
return ResultCode.InvalidCharInfo;
}
if (charInfo.Type == 1)
{
return ResultCode.InvalidOperationOnSpecialMii;
}
CoreData coreData = new();
coreData.SetFromCharInfo(charInfo);
StoreData storeData;
do
{
storeData = StoreData.BuildFromCoreData(utilityImpl, coreData);
}
while (_database.GetIndexByCreatorId(out _, storeData.CreateId));
return AddOrReplace(metadata, storeData);
}
public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId)
{
if (!_database.GetIndexByCreatorId(out int index, createId))

View File

@@ -54,9 +54,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected override ResultCode UpdateLatest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo)
{
newCharInfo = default;
return _database.UpdateLatest(_metadata, oldCharInfo, flag, newCharInfo);
return _database.UpdateLatest(_metadata, oldCharInfo, flag, out newCharInfo);
}
protected override ResultCode BuildRandom(Age age, Gender gender, Race race, out CharInfo charInfo)
@@ -113,14 +111,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected override ResultCode UpdateLatest1(StoreData oldStoreData, SourceFlag flag, out StoreData newStoreData)
{
newStoreData = default;
if (!_isSystem)
{
newStoreData = default;
return ResultCode.PermissionDenied;
}
return _database.UpdateLatest(_metadata, oldStoreData, flag, newStoreData);
return _database.UpdateLatest(_metadata, oldStoreData, flag, out newStoreData);
}
protected override ResultCode FindIndex(CreateId createId, bool isSpecial, out int index)
@@ -262,5 +260,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
{
return _database.ConvertCharInfoToCoreData(charInfo, out coreData);
}
protected override ResultCode Append(CharInfo charInfo)
{
return _database.Append(_metadata, charInfo);
}
}
}

View File

@@ -340,6 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
return result;
}
[CommandCmif(26)] // 10.2.0+
// Append(nn::mii::CharInfo char_info)
public ResultCode Append(ServiceCtx context)
{
CharInfo charInfo = context.RequestData.ReadStruct<CharInfo>();
return Append(charInfo);
}
private Span<byte> CreateByteSpanFromBuffer(ServiceCtx context, IpcBuffDesc ipcBuff, bool isOutput)
{
byte[] rawData;
@@ -421,5 +430,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected abstract ResultCode ConvertCoreDataToCharInfo(CoreData coreData, out CharInfo charInfo);
protected abstract ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData);
protected abstract ResultCode Append(CharInfo charInfo);
}
}

View File

@@ -0,0 +1,187 @@
using NUnit.Framework;
using Ryujinx.HLE.HOS.Services.Caps;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SkiaSharp;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Ryujinx.Tests.HLE
{
public class CaptureManagerTests
{
private const int ScreenshotWidth = 1280;
private const int ScreenshotHeight = 720;
private const int BytesPerPixel = 4;
private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * BytesPerPixel; // 0x384000
private const int PaddedScreenshotDataSize = ScreenshotWidth * 768 * BytesPerPixel; // 0x3C0000
[Test]
public void SaveScreenShotRejectsBufferSmallerThan720p()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = new byte[ScreenshotDataSize - 1];
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
Assert.That(result, Is.EqualTo(ResultCode.NullInputBuffer));
Assert.That(Directory.Exists(Path.Combine(tempSdCard.Path, "Nintendo", "Album")), Is.False);
}
[Test]
public void SaveScreenShotAcceptsExact720pBuffer()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(ScreenshotDataSize);
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out ApplicationAlbumEntry applicationAlbumEntry);
string filePath = GetSingleAlbumFile(tempSdCard.Path);
using SKBitmap bitmap = SKBitmap.Decode(filePath);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth));
Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight));
Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000));
Assert.That(applicationAlbumEntry.AlbumStorage, Is.EqualTo(AlbumStorage.Sd));
Assert.That(applicationAlbumEntry.ContentType, Is.EqualTo(ContentType.Screenshot));
Assert.That(applicationAlbumEntry.Unknown0x1f, Is.EqualTo(1));
});
}
[Test]
public void SaveScreenShotAcceptsBufferLargerThan720p()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(PaddedScreenshotDataSize);
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out ApplicationAlbumEntry applicationAlbumEntry);
string filePath = GetSingleAlbumFile(tempSdCard.Path);
using SKBitmap bitmap = SKBitmap.Decode(filePath);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth));
Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight));
Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000));
});
}
[Test]
public void SaveScreenShotCreatesUniqueFileNamesForRepeatedSaves()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(ScreenshotDataSize);
ResultCode firstResult = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
ResultCode secondResult = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
string[] files = Directory.GetFiles(
Path.Combine(tempSdCard.Path, "Nintendo", "Album"),
"*.jpg",
SearchOption.AllDirectories);
Assert.Multiple(() =>
{
Assert.That(firstResult, Is.EqualTo(ResultCode.Success));
Assert.That(secondResult, Is.EqualTo(ResultCode.Success));
Assert.That(files, Has.Length.EqualTo(2));
});
}
private static CaptureManager CreateCaptureManager(string sdCardPath)
{
CaptureManager captureManager = (CaptureManager)RuntimeHelpers.GetUninitializedObject(typeof(CaptureManager));
typeof(CaptureManager)
.GetField("_sdCardPath", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(captureManager, sdCardPath);
return captureManager;
}
private static string GetSingleAlbumFile(string sdCardPath)
{
string albumPath = Path.Combine(sdCardPath, "Nintendo", "Album");
string[] files = Directory.GetFiles(albumPath, "*.jpg", SearchOption.AllDirectories);
Assert.That(files, Has.Length.EqualTo(1));
return files.Single();
}
private static byte[] CreateTestPattern(int size)
{
byte[] data = new byte[size];
int pixelCount = size / BytesPerPixel;
for (int i = 0; i < pixelCount; i++)
{
int x = i % ScreenshotWidth;
int y = i / ScreenshotWidth;
data[(i * 4) + 0] = (byte)(x & 0xff);
data[(i * 4) + 1] = (byte)(y & 0xff);
data[(i * 4) + 2] = 0x80;
data[(i * 4) + 3] = 0xff;
}
return data;
}
private sealed class TempSdCard : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"sdcard-" + Guid.NewGuid());
public void Dispose()
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
}
}

View File

@@ -0,0 +1,122 @@
using System.Reflection;
using NUnit.Framework;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Mii.StaticService;
using Ryujinx.HLE.HOS.Services.Mii.Types;
namespace Ryujinx.Tests.HLE
{
public class MiiDatabaseTests
{
[Test]
public void UpdateLatestReturnsStoredCharInfo()
{
DatabaseImpl database = new();
StoreData storedData = StoreData.BuildDefault(new UtilityImpl(new TickSource(19200000)), 0);
MiiDatabaseManager databaseManager = GetDatabaseManager(database);
NintendoFigurineDatabase figurineDatabase = new();
figurineDatabase.Format();
figurineDatabase.Add(storedData);
SetFigurineDatabase(databaseManager, figurineDatabase);
TestDatabaseService service = new(database);
CharInfo oldCharInfo = new();
oldCharInfo.SetFromStoreData(storedData);
oldCharInfo.Height--;
ResultCode result = service.UpdateLatestForTest(oldCharInfo, SourceFlag.Database, out CharInfo newCharInfo);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(newCharInfo.CreateId, Is.EqualTo(oldCharInfo.CreateId));
Assert.That(newCharInfo.Height, Is.EqualTo(storedData.CoreData.Height));
Assert.That(newCharInfo.IsValid(), Is.True);
});
}
[Test]
public void AppendAddsRegularCharInfoToDatabase()
{
DatabaseImpl database = new();
UtilityImpl utilityImpl = new(new TickSource(19200000));
SetUtilityImpl(database, utilityImpl);
MiiDatabaseManager databaseManager = GetDatabaseManager(database);
SetFigurineDatabase(databaseManager, CreateFormattedDatabase());
StoreData defaultStoreData = StoreData.BuildDefault(utilityImpl, 0);
Assert.Multiple(() =>
{
Assert.That(defaultStoreData.CoreData.IsValid(), Is.True);
Assert.That(defaultStoreData.IsValidDataCrc(), Is.True);
Assert.That(defaultStoreData.IsValidDeviceCrc(), Is.True);
Assert.That(defaultStoreData.IsValid(), Is.True);
});
CharInfo charInfo = new();
charInfo.SetFromStoreData(defaultStoreData);
DatabaseSessionMetadata metadata = database.CreateSessionMetadata(new SpecialMiiKeyCode());
ResultCode result = databaseManager.Append(metadata, utilityImpl, charInfo);
int count = databaseManager.GetCount(metadata);
databaseManager.Get(metadata, 0, out StoreData storedData);
CoreData expectedCoreData = new();
expectedCoreData.SetFromCharInfo(charInfo);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(count, Is.EqualTo(1));
Assert.That(storedData.IsValid(), Is.True);
Assert.That(storedData.CreateId, Is.Not.EqualTo(charInfo.CreateId));
Assert.That(storedData.CoreData, Is.EqualTo(expectedCoreData));
});
}
private sealed class TestDatabaseService(DatabaseImpl database) : DatabaseServiceImpl(database, true, new SpecialMiiKeyCode())
{
public ResultCode UpdateLatestForTest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo)
{
return UpdateLatest(oldCharInfo, flag, out newCharInfo);
}
}
private static MiiDatabaseManager GetDatabaseManager(DatabaseImpl database)
{
return (MiiDatabaseManager)typeof(DatabaseImpl)
.GetField("_miiDatabase", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(database);
}
private static void SetFigurineDatabase(MiiDatabaseManager databaseManager, NintendoFigurineDatabase figurineDatabase)
{
typeof(MiiDatabaseManager)
.GetField("_database", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(databaseManager, figurineDatabase);
}
private static NintendoFigurineDatabase CreateFormattedDatabase()
{
NintendoFigurineDatabase figurineDatabase = new();
figurineDatabase.Format();
return figurineDatabase;
}
private static void SetUtilityImpl(DatabaseImpl database, UtilityImpl utilityImpl)
{
typeof(DatabaseImpl)
.GetField("_utilityImpl", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(database, utilityImpl);
}
}
}

View File

@@ -622,15 +622,15 @@ namespace Ryujinx.Ava.Systems
// If the GPU has no work and is cancelled, we need to handle that as well.
WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
_gpuCancellationTokenSource.Dispose();
// Waiting for work to be finished before we dispose.
if (_renderingStarted)
{
// Waiting for work to be finished before we dispose.
Device.Gpu.WaitUntilGpuReady();
}
_gpuDoneEvent.Dispose();
_gpuCancellationTokenSource.Dispose();
DisposeGpu();
AppExit?.Invoke(this, EventArgs.Empty);
@@ -1095,51 +1095,56 @@ namespace Ryujinx.Ava.Systems
Device.Gpu.Renderer.RunLoop(() =>
{
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_renderer.Window.ChangeVSyncMode(Device.VSyncMode);
while (_isActive)
try
{
_ticks += _chrono.ElapsedTicks;
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_chrono.Restart();
_renderer.Window.ChangeVSyncMode(Device.VSyncMode);
if (Device.WaitFifo())
while (_isActive)
{
Device.Statistics.RecordFifoStart();
Device.ProcessFrame();
Device.Statistics.RecordFifoEnd();
}
_ticks += _chrono.ElapsedTicks;
while (Device.ConsumeFrameAvailable())
{
if (!_renderingStarted)
_chrono.Restart();
if (Device.WaitFifo())
{
_renderingStarted = true;
_viewModel.SwitchToRenderer(false);
InitStatus();
Device.Statistics.RecordFifoStart();
Device.ProcessFrame();
Device.Statistics.RecordFifoEnd();
}
Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
}
while (Device.ConsumeFrameAvailable())
{
if (!_renderingStarted)
{
_renderingStarted = true;
_viewModel.SwitchToRenderer(false);
InitStatus();
}
if (_ticks >= _ticksPerFrame)
{
UpdateStatus();
Device.PresentFrame(() =>
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
}
if (_ticks >= _ticksPerFrame)
{
UpdateStatus();
}
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
finally
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
}
_gpuDoneEvent.Set();
}
_gpuDoneEvent.Set();
});
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true);

View File

@@ -8,6 +8,12 @@ namespace Ryujinx.Ava.UI.Helpers
internal partial class Win32NativeInterop
{
internal const int GWLP_WNDPROC = -4;
internal const int GWL_STYLE = -16;
internal const int GWL_EXSTYLE = -20;
internal const uint WS_OVERLAPPEDWINDOW = 0x00CF0000;
internal const uint WS_POPUP = 0x80000000;
internal const uint WS_VISIBLE = 0x10000000;
[Flags]
public enum ClassStyles : uint
@@ -107,9 +113,29 @@ namespace Ryujinx.Ava.UI.Helpers
nint hInstance,
nint lpParam);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW")]
public static partial nint GetWindowLongPtrW(nint hWnd, int nIndex);
[LibraryImport("user32.dll", SetLastError = true)]
public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SetWindowPos(
nint hWnd,
nint hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
internal const uint SWP_NOZORDER = 0x0004;
internal const uint SWP_NOACTIVATE = 0x0010;
internal const uint SWP_FRAMECHANGED = 0x0020;
internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_NOSIZE = 0x0001;
[LibraryImport("user32.dll", SetLastError = true)]
public static partial ushort GetAsyncKeyState(int nVirtKey);

View File

@@ -5,6 +5,7 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using System.Runtime.Versioning;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
@@ -2014,7 +2015,7 @@ namespace Ryujinx.Ava.UI.ViewModels
LastFullscreenToggle = Environment.TickCount64;
if (WindowState is not WindowState.Normal)
if (WindowState is WindowState.FullScreen)
{
WindowState = WindowState.Normal;
Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowOldUI;
@@ -2023,21 +2024,74 @@ namespace Ryujinx.Ava.UI.ViewModels
{
ShowMenuAndStatusBar = true;
}
if (OperatingSystem.IsWindows())
{
RestoreWindowFromFullscreen();
}
}
else
{
WindowState = WindowState.FullScreen;
Window.TitleBar.ExtendsContentIntoTitleBar = true;
if (IsGameRunning)
{
ShowMenuAndStatusBar = false;
}
if (OperatingSystem.IsWindows())
{
MakeWindowFullscreen();
}
else
{
WindowState = WindowState.FullScreen;
}
}
IsFullScreen = WindowState is WindowState.FullScreen;
}
private nint _savedWindowStyle;
[SupportedOSPlatform("windows")]
private void MakeWindowFullscreen()
{
nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero;
if (hwnd == nint.Zero) return;
// Save current style and placement
_savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE);
// Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE
Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE,
unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE)));
Avalonia.Platform.Screen? screen = Window.Screens.ScreenFromVisual(Window);
int w = screen?.Bounds.Width ?? 0;
int h = screen?.Bounds.Height ?? 0;
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, w, h,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED);
WindowState = WindowState.FullScreen;
}
[SupportedOSPlatform("windows")]
private void RestoreWindowFromFullscreen()
{
nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero;
if (hwnd == nint.Zero) return;
// Restore original window style
Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, _savedWindowStyle);
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE |
Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE);
}
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);