11 Commits
Author SHA1 Message Date
Neo 53d32a761e Improve "Add raw copy dependencies for incompatible textures"
Initial PR was made by Avan for Ryubing.

This allows for Trails in the Sky 1st Chapter (and by extension Trails through Daybreak, Trails through Daybreak II, Trails Beyond the Horizon, and the future Trails in the Sky 2nd Chapter – tested via demo – to be fully playable.

Initial PR caused issues on macOS devices, specifically crashing in certain games (such as Mario Kart 8 Deluxe and the aforementioned Trails games). It was reverted on the original Ryubing project (at the time of writing this description) for causing small rendering issues on The Legend of Zelda: Breath of the Wild.

The rendering issues do not appear on macOS.

This commit improves the original PR by eliminating said macOS crash.

Trails in the Sky 1st writes its exposure/brightness value through an R32G32Float 1x1 texture and later reads the raw bits back through an R32Uint 2x1 texture mapped to the same guest memory. The two texture formats/dimensions are fully incompatible as texture views, so they end
up as separate host textures, and nothing kept their contents coherent - the reader saw stale zeroed data, making the exposure calculation (and the rendered image) too dark. (This is the initial fix for the game)

TextureGroup already had a raw-copy dependency mechanism intended to
handle this class of alias (TextureDependency, TextureGroupHandle
raw-copy plumbing), gated by CanCreateRawCopyDependency's strict same-guest-memory checks. This commit fixes three bugs that kept that mechanism from working correctly (and thus crashing Trails and other games, such as Mario Kart 8 Deluxe):

(1) TextureGroup.InitializeOverlaps() and TextureGroup.RegisterIncompatibleOverlap() only forwarded overlaps to CreateCopyDependency() when compatibility was LayoutIncompatible or better, silently excluding the fully Incompatible case the raw-copy path exists for. Widened both guards to let Incompatible overlaps through so CanCreateRawCopyDependency actually gets a chance to run.

(2) Once Incompatible overlaps were allowed through, CreateCopyDependency(TextureGroup, ...) could still fall back to a regular, non-raw textureCopy for such pairs, since ViewLayoutCompatible/CopySizeMatches only check byte size and were never meant to reason about fully incompatible pairs (e.g. a depth format aliasing a color format with the same byte size). On Vulkan running through MoltenVK on macOS, that non-raw copy path requires a pixel-format-reinterpreting texture view, and MoltenVK/Metal refuses to create any view onto a depth-format texture ("not castable"), aborting emulation. textureCopy is now forced false for Incompatible pairs, leaving raw copy (which already excludes depth/stencil formats) as the only route for that severity level.

(3) TextureGroupHandle.Inherit() copied a handle's pending DeferredCopy to the new handle when a view was recreated, but did not copy DeferredCopyRaw alongside it. A handle that inherited a pending raw copy would then execute it through the regular (non-raw) CopyTo path once acknowledged, reinterpreting the source bytes with the wrong row layout and corrupting the image - visible as vertical flickering stripes whenever a view happened to be recreated with a raw copy still pending. DeferredCopyRaw is now carried over together with DeferredCopy.

(As mentioned earlier, vertical stripes aren't present on macOS, so (3) doesn't negatively have an effect on macOS in any way).
2026-08-29 10:49:56 -05:00
MaxandMythrax 02d8eeb18f HLE: Implemented ILockAccessor and ICommonStateGetter commands and stubs for Virtual Boy – Nintendo Classics #3
- Moves emulation past the initial frame by providing the necessary inputs the game expects. Apparently games like to spin until they get what they want.

Co-authored-by: Mythrax <mythrax@mythrax-rs.org>
2026-08-29 10:49:56 -05:00
Babib3l 54e2f79c16 Hotfix for the PTC version, it's adjacent comment and PTC writer logging 2026-08-29 10:49:21 -05:00
Babib3l 77277442a9 Wire "Start games in fullscreen" option to use the new fullscreen behaviour
Final (hopefully) fix for https://github.com/Ryubing/Issues/issues/415

(cherry picked from commit 7be8829b0a)
2026-08-29 10:39:39 -05:00
Babib3l b707c73f83 River 2 : HLE: Use per-program ownership for PTC disk caches
This PR threads process/program identity into PTC disk cache initialization so cache ownership is selected from the launched process rather than global/shared application state.

Previously, the PTC initialization path only propagated loose title/version information into the CPU layer. That was mostly fine for a single launched application, but will introduce many problems once multiple programs can be launched during the same session, given later processes could inherit cache identity from the first loaded application.
To address this issue, this PR introduces a `PtcCacheInfo` payload and applies it through the process context, CPU context, translator, and PTC initialization paths. Cache ownership is now resolved once the kernel process PID is known and includes:

- PID
- Program ID / Title ID
- Application ID
- Program index
- Display version
- Process kind
- Cache selector

`PtcCacheInfo` now owns its default title/application/version values, and `Ptc` uses that cache info directly instead of mirroring title/version fields internally.

The persistent cache key itself does remains title/version/selector based rather than PID based, so caches remain reusable across launches while still being selected from the correct process context. PID is only used for diagnostics and ownership tracing in logs.

Additional PTC logging has also been added to report cache ownership and selected paths during PTC initialization, Profiling info load/save and translation cache load/save

Both the PTC and profiler internal versions were bumped (a bunch of times lol).

(cherry picked from commit 7101f52c01)
2026-08-24 13:58:01 -05:00
avan 235fb5f8c2 Fix view compatibility for compressed textures with different logical sizes
It has been confirmed that Divinity: Original Sin 2 may describe the same or overlapping guest GPU memory using BCn-compressed textures with different logical dimensions, such as 104×104 and 102×102.

BCn formats store texture data in blocks, with each BC block covering 4×4 texels. After rounding the dimensions up to complete blocks, both 104×104 and 102×102 textures require a 26×26 grid of BC blocks. As a result, they have the same block footprint at the base mip level. Ryujinx originally determined size compatibility in TextureCompatibility.ViewSizeMatches() primarily from this block footprint, which could cause these textures to be classified as Full view-compatible and allowed to share the same Vulkan VkImage directly.

However, because the two textures have different logical dimensions, their mip chains are also different. For example, at mip level 3, a 104×104 texture is reduced to 13×13, while a 102×102 texture is reduced to 12×12. If a 104×104 child texture view shares the backing image of a 102×102 parent texture, a subsequent full mip upload may attempt to write 13×13 compressed data into a Vulkan image subresource whose actual dimensions are only 12×12.

This produces a compressed buffer-to-image copy that does not match the geometry of the backing image. After the invalid command is submitted to the GPU, the Vulkan driver may asynchronously report VK_ERROR_DEVICE_LOST during command buffer submission, waiting, or presentation. In Ryujinx, this eventually appears as: VulkanException: Unexpected API error "ErrorDeviceLost".

The fix adds a logical-dimension check to TextureCompatibility.ViewSizeMatches(). If either the parent or child uses a compressed texture format, and the actual width or height of the corresponding parent mip does not match the logical width or height of the child, the relationship is no longer classified as Full view-compatible. Instead, it is downgraded to CopyOnly.

CopyOnly indicates that the two textures may still describe the same or overlapping guest memory and that their contents must remain synchronized, but they cannot directly share a single fixed-size Vulkan image. Ryujinx instead allows each texture to retain a host texture matching its own logical dimensions and synchronizes their contents through the existing copy-dependency mechanism.

As a result, the 13×13 mip of the 104×104 texture is uploaded to an actual 13×13 destination mip, while the 12×12 mip of the 102×102 texture is uploaded to an actual 12×12 destination mip.

(cherry picked from commit e80fc00462)
2026-08-24 13:58:01 -05:00
avan c2cbd53235 Fix bindless elimination failures observed in OCTOPATH TRAVELER 0:
Failed to find handle source for bindless access of type "textureBuffer".

(cherry picked from commit 33cbd29c23)
2026-08-24 13:58:01 -05:00
avan 4f3c3a7b5d Fix OpenGL program relaunch
The Enhanced and Classic versions of FFT are different program indices within the same application. When the Classic version is launched, the game uses ExecuteProgram to stop the current program, trigger DisposeGpu, create a new renderer and OpenGL context, and then launch the requested program.

Previously, when AppHost executed DisposeGpu, it attempted to bind the old OpenGL context and then called Device.DisposeGpu to destroy GPU resources. During the program relaunch flow, however, the old RendererHost may already have been removed from the visual tree, causing its native window to become detached.

If the old OpenGL context can no longer be bound, a ContextException is thrown, preventing the subsequent AppExit and program relaunch flow from continuing.

The updated implementation handles this case by detecting a ContextException while binding the OpenGL context when ShouldRestart is true. It then skips the destruction of GPU resources that depend on the old OpenGL context, allowing the old context to be released together with the old window. The AppExit and program relaunch flow can then continue without being interrupted.

(cherry picked from commit cfc7c6039f)
2026-08-24 13:58:01 -05:00
avan 2dc1894e1e Fix Vulkan/OpenGL attachmentless rendering
Some fragment passes used by FFT do not have any color or depth/stencil attachments. Instead, the fragment shader writes the results directly to storage images (on OpenGL: through imageStore).

### Vulkan
When SetImage is called and FramebufferParams has no attachments, its virtual size must be updated using the width, height, and layer count of the fragment storage image. Otherwise, the attachmentless framebuffer may retain an incorrect 1x1 extent, causing backgrounds, logos, UI elements, and other rendered content to be missing.

If the first draw occurs before the storage-image descriptor is rebound, SetImage cannot yet provide the correct storage-image dimensions. Therefore, when RecreateGraphicsPipelineIfNeeded finds that FramebufferParams has no attachments and still uses the default 1x1 extent, it initializes the framebuffer dimensions from the active viewport. Otherwise, the first Vulkan draw may be restricted to a 1x1 area and render incorrectly.

The storage-image extent is therefore used as the authoritative size for FramebufferParams, while the active viewport is used as a fallback when the storage-image extent is not yet available before the first draw.

### OpenGL
A storage image is not a framebuffer attachment, so OpenGL cannot derive the framebuffer width and height from it. When the framebuffer has no attachments but a viewport with valid dimensions has already been defined, Pipeline.PreDraw must set the default framebuffer width and height from the active viewport.

Without non-zero default width and height values, the attachmentless framebuffer remains incomplete. Drawing with that framebuffer results in InvalidFramebufferOperation, so rasterization and fragment shader execution do not occur even when the storage images are bound correctly.

(cherry picked from commit ac6db0fe76)
2026-08-24 13:58:00 -05:00
Babib3l db4799e7ab feature: Add GpuDriverVersion to GPU info logging
- The Vulkan backend now reports the driver name and version using VK_KHR_driver_properties when available, with fallbacks for raw Vulkan driverVersion parsing. Intel Windows drivers also get the full package-style version from the driver UUID when exposed by the driver.
  OpenGL now carries its existing OpenGL version string through HardwareInfo.GpuDriverVersion.
2026-08-24 13:58:00 -05:00
LotP 06f049b874 Fix a crash when no controller is connected
Co-authored-by: LotP1 <68976644+LotP1@users.noreply.github.com>
(cherry picked from commit 1a1fe53535)
2026-08-24 13:58:00 -05:00
30 changed files with 724 additions and 108 deletions
+29 -21
View File
@@ -31,14 +31,11 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 7010; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 7031; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
private const string TitleIdTextDefault = "0000000000000000";
private const string DisplayVersionDefault = "0";
public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
@@ -64,8 +61,7 @@ namespace ARMeilleure.Translation.PTC
private bool _disposed;
public string TitleIdText { get; private set; }
public string DisplayVersion { get; private set; }
public PtcCacheInfo CacheInfo { get; private set; }
private MemoryManagerType _memoryMode;
@@ -92,8 +88,7 @@ namespace ARMeilleure.Translation.PTC
_disposed = false;
TitleIdText = TitleIdTextDefault;
DisplayVersion = DisplayVersionDefault;
CacheInfo = new PtcCacheInfo(0, null, null, 0, null, "Unknown", "default");
CachePathActual = string.Empty;
CachePathBackup = string.Empty;
@@ -101,20 +96,24 @@ namespace ARMeilleure.Translation.PTC
Disable();
}
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector)
public void Initialize(PtcCacheInfo cacheInfo, bool enabled, MemoryManagerType memoryMode)
{
Wait();
Profiler.Wait();
Profiler.ClearEntries();
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t (title: {titleIdText}, version: '{displayVersion}', selector: '{cacheSelector}', enabled: {enabled}).");
CacheInfo = cacheInfo;
if (!enabled || string.IsNullOrEmpty(titleIdText) || titleIdText == TitleIdTextDefault)
Logger.Info?.Print(
LogClass.Ptc,
$"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t " +
$"(pid: {cacheInfo.ProcessId}, title: {cacheInfo.TitleIdText}, application: {cacheInfo.ApplicationIdText}, " +
$"programIndex: {cacheInfo.ProgramIndex}, version: '{cacheInfo.DisplayVersion}', kind: {cacheInfo.ProcessKind}, " +
$"selector: '{cacheInfo.CacheSelector}', key: '{cacheInfo.CacheKey}', enabled: {enabled}).");
if (!enabled || cacheInfo.TitleIdText == PtcCacheInfo.TitleIdTextDefault)
{
TitleIdText = TitleIdTextDefault;
DisplayVersion = DisplayVersionDefault;
CachePathActual = string.Empty;
CachePathBackup = string.Empty;
@@ -123,12 +122,10 @@ namespace ARMeilleure.Translation.PTC
return;
}
TitleIdText = titleIdText;
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
_memoryMode = memoryMode;
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, CacheInfo.TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, CacheInfo.TitleIdText, "cache", "cpu", BackupDir);
if (!Directory.Exists(workPathActual))
{
@@ -140,8 +137,14 @@ namespace ARMeilleure.Translation.PTC
Directory.CreateDirectory(workPathBackup);
}
CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector;
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector;
CachePathActual = Path.Combine(workPathActual, CacheInfo.DisplayVersion) + "-" + CacheInfo.CacheSelector;
CachePathBackup = Path.Combine(workPathBackup, CacheInfo.DisplayVersion) + "-" + CacheInfo.CacheSelector;
Logger.Info?.Print(
LogClass.Ptc,
$"PPTC cache owner selected (pid: {CacheInfo.ProcessId}, title: {CacheInfo.TitleIdText}, application: {CacheInfo.ApplicationIdText}, " +
$"version: '{CacheInfo.DisplayVersion}', kind: {CacheInfo.ProcessKind}, selector: '{CacheInfo.CacheSelector}', " +
$"key: '{CacheInfo.CacheKey}', path: '{CachePathActual}').");
PreLoad();
Profiler.PreLoad();
@@ -364,7 +367,12 @@ namespace ARMeilleure.Translation.PTC
long fileSize = new FileInfo(fileName).Length;
Logger.Info?.Print(LogClass.Ptc, $"{(isBackup ? "Loaded Backup Translation Cache" : "Loaded Translation Cache")} (size: {fileSize} bytes, translated functions: {GetEntriesCount()}).");
Logger.Info?.Print(
LogClass.Ptc,
$"{(isBackup ? "Loaded Backup Translation Cache" : "Loaded Translation Cache")} " +
$"(pid: {CacheInfo.ProcessId}, title: {CacheInfo.TitleIdText}, version: '{CacheInfo.DisplayVersion}', kind: {CacheInfo.ProcessKind}, " +
$"selector: '{CacheInfo.CacheSelector}', key: '{CacheInfo.CacheKey}', path: '{fileName}', " +
$"size: {fileSize} bytes, translated functions: {GetEntriesCount()}).");
return true;
}
@@ -0,0 +1,37 @@
namespace ARMeilleure.Translation.PTC
{
public readonly struct PtcCacheInfo
{
public const string TitleIdTextDefault = "0000000000000000";
public const string ApplicationIdTextDefault = "0000000000000000";
public const string DisplayVersionDefault = "0";
public ulong ProcessId { get; }
public string TitleIdText { get; }
public string ApplicationIdText { get; }
public byte ProgramIndex { get; }
public string DisplayVersion { get; }
public string ProcessKind { get; }
public string CacheSelector { get; }
public string CacheKey => $"{DisplayVersion}-{CacheSelector}";
public PtcCacheInfo(
ulong processId,
string titleIdText,
string applicationIdText,
byte programIndex,
string displayVersion,
string processKind,
string cacheSelector)
{
ProcessId = processId;
TitleIdText = !string.IsNullOrEmpty(titleIdText) ? titleIdText : TitleIdTextDefault;
ApplicationIdText = !string.IsNullOrEmpty(applicationIdText) ? applicationIdText : ApplicationIdTextDefault;
ProgramIndex = programIndex;
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
ProcessKind = processKind ?? string.Empty;
CacheSelector = string.IsNullOrEmpty(cacheSelector) ? "default" : cacheSelector;
}
}
}
+12 -3
View File
@@ -23,7 +23,7 @@ namespace ARMeilleure.Translation.PTC
{
private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
private const uint InternalVersion = 6698; //! Not to be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 7031; //! Not to be incremented manually for each change to the ARMeilleure project.
private static readonly uint[] _migrateInternalVersions =
[
@@ -254,7 +254,12 @@ namespace ARMeilleure.Translation.PTC
long fileSize = new FileInfo(fileName).Length;
Logger.Info?.Print(LogClass.Ptc, $"{(isBackup ? "Loaded Backup Profiling Info" : "Loaded Profiling Info")} (size: {fileSize} bytes, profiled functions: {ProfiledFuncs.Count}).");
Logger.Info?.Print(
LogClass.Ptc,
$"{(isBackup ? "Loaded Backup Profiling Info" : "Loaded Profiling Info")} " +
$"(pid: {_ptc.CacheInfo.ProcessId}, title: {_ptc.CacheInfo.TitleIdText}, version: '{_ptc.CacheInfo.DisplayVersion}', " +
$"kind: {_ptc.CacheInfo.ProcessKind}, selector: '{_ptc.CacheInfo.CacheSelector}', key: '{_ptc.CacheInfo.CacheKey}', " +
$"path: '{fileName}', size: {fileSize} bytes, profiled functions: {ProfiledFuncs.Count}).");
return true;
}
@@ -375,7 +380,11 @@ namespace ARMeilleure.Translation.PTC
if (fileSize != 0L)
{
Logger.Info?.Print(LogClass.Ptc, $"Saved Profiling Info (size: {fileSize} bytes, profiled functions: {profiledFuncsCount}).");
Logger.Info?.Print(
LogClass.Ptc,
$"Saved Profiling Info (pid: {_ptc.CacheInfo.ProcessId}, title: {_ptc.CacheInfo.TitleIdText}, version: '{_ptc.CacheInfo.DisplayVersion}', " +
$"kind: {_ptc.CacheInfo.ProcessKind}, selector: '{_ptc.CacheInfo.CacheSelector}', key: '{_ptc.CacheInfo.CacheKey}', " +
$"path: '{fileName}', size: {fileSize} bytes, profiled functions: {profiledFuncsCount}).");
}
}
+2 -2
View File
@@ -57,9 +57,9 @@ namespace ARMeilleure.Translation
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
public IPtcLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector);
_ptc.Initialize(cacheInfo, enabled, Memory.Type);
return _ptc;
}
+2 -1
View File
@@ -1,4 +1,5 @@
using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv
@@ -32,7 +33,7 @@ namespace Ryujinx.Cpu.AppleHv
{
}
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{
return new DummyDiskCacheLoadState();
}
+3 -3
View File
@@ -1,4 +1,5 @@
using System;
using ARMeilleure.Translation.PTC;
namespace Ryujinx.Cpu
{
@@ -44,11 +45,10 @@ namespace Ryujinx.Cpu
/// <remarks>
/// If the execution engine is recompiling guest code, this can be used to load cached code from disk.
/// </remarks>
/// <param name="titleIdText">Title ID of the application in padded hex form</param>
/// <param name="displayVersion">Version of the application</param>
/// <param name="cacheInfo">Identity and selector for the process-owned disk cache</param>
/// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param>
/// <returns>Disk cache load progress reporter and manager</returns>
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector);
IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled);
/// <summary>
/// Indicates that code has been loaded into guest memory, and that it might be executed in the future.
+3 -2
View File
@@ -1,6 +1,7 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu.Signal;
namespace Ryujinx.Cpu.Jit
@@ -49,9 +50,9 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector));
return new JitDiskCacheLoadState(_translator.LoadDiskCache(cacheInfo, enabled));
}
/// <inheritdoc/>
@@ -1,5 +1,6 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu.Jit;
using Ryujinx.Cpu.LightningJit.State;
@@ -51,7 +52,7 @@ namespace Ryujinx.Cpu.LightningJit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{
return new DummyDiskCacheLoadState();
}
+3 -1
View File
@@ -5,12 +5,14 @@ namespace Ryujinx.Graphics.GAL
public string GpuVendor { get; }
public string GpuModel { get; }
public string GpuDriver { get; }
public string GpuDriverVersion { get; }
public HardwareInfo(string gpuVendor, string gpuModel, string gpuDriver)
public HardwareInfo(string gpuVendor, string gpuModel, string gpuDriver, string gpuDriverVersion)
{
GpuVendor = gpuVendor;
GpuModel = gpuModel;
GpuDriver = gpuDriver;
GpuDriverVersion = gpuDriverVersion;
}
}
}
@@ -369,12 +369,15 @@ namespace Ryujinx.Graphics.Gpu.Image
}
// Some APIs align the width for copy and render target textures,
// so the width may not match in this case for different uses of the same texture.
// so the width may not match for different uses of the same texture.
// To account for this, we compare the aligned width here.
// We expect height to always match exactly, if the texture is the same.
// However, matching block footprints are not sufficient for compressed textures;
// their logical dimensions must also match.
if (alignedWidthMatches && lhsSize.Height == rhsSize.Height)
{
return (exact && lhsSize.Width != rhsSize.Width) || lhsSize.Width < rhsSize.Width
return ((lhs.FormatInfo.IsCompressed || rhs.FormatInfo.IsCompressed) &&
(Math.Max(1, lhs.Width >> level) != rhs.Width || Math.Max(1, lhs.Height >> level) != rhs.Height)) ||
(exact && lhsSize.Width != rhsSize.Width) || lhsSize.Width < rhsSize.Width
? TextureViewCompatibility.CopyOnly
: result;
}
+23 -11
View File
@@ -182,7 +182,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{
foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps)
{
if (overlap.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
// LayoutIncompatible and better may still use a regular texture copy dependency.
// Fully Incompatible pairs are not copy compatible in general, but may still qualify for an
// exact raw byte copy dependency (checked internally by CreateCopyDependency) when they map
// to exactly the same guest memory, such as differently typed/sized aliases of the same data.
if (overlap.Compatibility <= TextureViewCompatibility.Incompatible)
{
CreateCopyDependency(overlap.Group, false, overlap.Compatibility);
}
@@ -226,7 +230,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Flushes incompatible overlaps if the storage format requires it, and they have been modified.
/// This allows unsupported host formats to accept data written to format aliased textures.
@@ -659,7 +662,7 @@ namespace Ryujinx.Graphics.Gpu.Image
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
{
@@ -1049,7 +1052,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel];
int size = endOffset - offset;
List<RegionHandle> result = new();
List<RegionHandle> result = [];
for (int i = 0; i < TextureRange.Count; i++)
{
@@ -1163,7 +1166,6 @@ namespace Ryujinx.Graphics.Gpu.Image
SignalAllDirty();
}
/// <summary>
/// Removes a view from the group, removing it from all overlap lists.
/// </summary>
@@ -1385,7 +1387,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_is3D)
{
List<TextureGroupHandle> handlesList = new();
List<TextureGroupHandle> handlesList = [];
for (int i = 0; i < levelHandles; i++)
{
@@ -1468,8 +1470,8 @@ namespace Ryujinx.Graphics.Gpu.Image
// Get the location of each texture within its storage, so we can find the handles to apply the dependency to.
// This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture.
List<(int BaseHandle, int RegionCount)> targetRange = new();
List<(int BaseHandle, int RegionCount)> otherRange = new();
List<(int BaseHandle, int RegionCount)> targetRange = [];
List<(int BaseHandle, int RegionCount)> otherRange = [];
EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, _, _) =>
{
@@ -1601,7 +1603,15 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo info = Storage.Info;
TextureInfo otherInfo = other.Storage.Info;
bool textureCopy = TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
// ViewLayoutCompatible/CopySizeMatches only reason about textures with some genuine
// format relationship (LayoutIncompatible or better) - they are not aware of, and must
// never be used to justify, a plain texture-to-texture copy (which some backends
// implement via a reinterpreting view) between fully Incompatible aliases such as a
// depth format and an unrelated color format. For Incompatible pairs, only the strict
// raw byte copy dependency below (which explicitly excludes depth/stencil formats) may
// be used.
bool textureCopy = compatibility != TextureViewCompatibility.Incompatible &&
TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel);
if (textureCopy || rawCopy)
@@ -1659,9 +1669,12 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (!_incompatibleOverlaps.Any(overlap => overlap.Group == other.Group))
{
if (copy && other.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
if (copy && other.Compatibility <= TextureViewCompatibility.Incompatible)
{
// Any of the group's views may share compatibility, even if the parents do not fully.
// Fully Incompatible groups are also let through here, since CreateCopyDependency will
// fall back to an exact raw byte copy dependency for them when the strict requirements
// for one are met (see CanCreateRawCopyDependency).
CreateCopyDependency(other.Group, false, other.Compatibility);
}
@@ -1803,4 +1816,3 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
}
@@ -717,6 +717,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
DeferredCopy = old.DeferredCopy;
DeferredCopyRaw = old.DeferredCopyRaw;
}
}
+36 -2
View File
@@ -20,6 +20,27 @@ namespace Ryujinx.Graphics.OpenGL
private int _colorsCount;
private bool _dualSourceBlend;
public bool HasAttachments
{
get
{
if (_depthStencil != null)
{
return true;
}
for (int index = 0; index < _colors.Length; index++)
{
if (_colors[index] != null)
{
return true;
}
}
return false;
}
}
public Framebuffer()
{
Handle = GL.GenFramebuffer();
@@ -34,6 +55,12 @@ namespace Ryujinx.Graphics.OpenGL
return Handle;
}
public void SetDefaultSize(int width, int height)
{
GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultWidth, Math.Max(1, width));
GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultHeight, Math.Max(1, height));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AttachColor(int index, TextureView color)
{
@@ -105,13 +132,20 @@ namespace Ryujinx.Graphics.OpenGL
_colorsCount = colorsCount;
}
private static void SetDrawBuffersImpl(int colorsCount)
private void SetDrawBuffersImpl(int colorsCount)
{
DrawBuffersEnum[] drawBuffers = new DrawBuffersEnum[colorsCount];
for (int index = 0; index < colorsCount; index++)
{
drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index;
if (_colors[index] != null)
{
drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index;
}
else
{
drawBuffers[index] = DrawBuffersEnum.None;
}
}
GL.DrawBuffers(colorsCount, drawBuffers);
@@ -133,7 +133,7 @@ namespace Ryujinx.Graphics.OpenGL
public HardwareInfo GetHardwareInfo()
{
return new HardwareInfo(GpuVendor, GpuRenderer, GpuVendor); // OpenGL does not provide a driver name, vendor name is closest analogue.
return new HardwareInfo(GpuVendor, GpuRenderer, GpuVendor, GpuVersion); // OpenGL does not provide a driver name, vendor name is closest analogue.
}
public PinnedSpan<byte> GetBufferData(BufferHandle buffer, int offset, int size)
+5
View File
@@ -1537,6 +1537,11 @@ namespace Ryujinx.Graphics.OpenGL
{
DrawCount++;
if (!_framebuffer.HasAttachments && _viewportArray.Length >= 4)
{
_framebuffer.SetDefaultSize((int)_viewportArray[2], (int)_viewportArray[3]);
}
_unit0Texture?.Bind(0);
}
@@ -70,19 +70,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
Operand bindlessHandle = texOp.GetSource(0);
if (bindlessHandle.AsgOp is PhiNode phi)
{
for (int srcIndex = 0; srcIndex < phi.SourcesCount; srcIndex++)
{
Operand phiSource = phi.GetSource(srcIndex);
if (phiSource.AsgOp is not PhiNode && !IsBindlessAccessAllowed(phiSource))
{
return false;
}
}
}
else if (!IsBindlessAccessAllowed(bindlessHandle))
if (!IsBindlessAccessAllowed(bindlessHandle))
{
return false;
}
@@ -100,7 +88,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
texOp.SetSource(0, textureIndex);
bool hasSampler = !texOp.Inst.IsImage();
// Buffer textures use a TIC/BufferView only and never consume a sampler.
// Avoiding a sampler descriptor array here is especially important for bindless
// texture buffers, where the array can span the entire sampler pool for no benefit.
bool hasSampler = !texOp.Inst.IsImage() && texOp.Type != SamplerType.TextureBuffer;
SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst,
@@ -143,26 +134,112 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
private static bool IsBindlessAccessAllowed(Operand bindlessHandle)
{
if (bindlessHandle.Type == OperandType.ConstantBuffer)
{
// Bindless access with handles from constant buffer is allowed.
// Walk only SSA merges and integer operations that can transparently construct
// or select a packed texture/sampler handle. Do not walk arbitrary operations:
// finding an unrelated resource load elsewhere in the SSA graph must not enable
// descriptor-array access for this handle.
const int MaxVisitedOperands = 256;
const int MaxDepth = 64;
return true;
Stack<(Operand Operand, int Depth)> work = new();
HashSet<Operand> visited = new();
work.Push((bindlessHandle, 0));
while (work.Count != 0 && visited.Count < MaxVisitedOperands)
{
(Operand operand, int depth) = work.Pop();
if (operand == null || depth > MaxDepth || !visited.Add(operand))
{
continue;
}
if (operand.Type == OperandType.ConstantBuffer)
{
// Constant buffers are accepted by the existing direct-handle path and
// remain accepted when SSA merges or integer handle arithmetic obscure them.
return true;
}
if (operand.AsgOp is PhiNode phi)
{
for (int index = 0; index < phi.SourcesCount; index++)
{
Operand source = phi.GetSource(index);
if (source.Type != OperandType.Undefined)
{
work.Push((source, depth + 1));
}
}
continue;
}
if (operand.AsgOp is not Operation operation)
{
continue;
}
Instruction inst = operation.Inst & Instruction.Mask;
if (inst == Instruction.Load)
{
// Preserve the performance restriction. Static CBUF operands are already
// accepted above; accept their dynamically indexed Load form as the same
// resource class, plus the existing shader-input and storage-buffer sources.
// Other loads remain traversal barriers and cannot authorize a pool array.
if (operation.StorageKind == StorageKind.ConstantBuffer ||
operation.StorageKind == StorageKind.Input ||
operation.StorageKind == StorageKind.StorageBuffer)
{
return true;
}
continue;
}
if (!IsHandleConstructionOperation(inst))
{
// Texture/image operations, arbitrary loads, floating-point conversions,
// calls and other unrelated calculations are deliberate traversal barriers.
continue;
}
for (int index = 0; index < operation.SourcesCount; index++)
{
work.Push((operation.GetSource(index), depth + 1));
}
}
if (bindlessHandle.AsgOp is not Operation handleOp ||
handleOp.Inst != Instruction.Load ||
(handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
{
// Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
// This is an artificial limitation to prevent it from being used in cases where it
// would have a large performance impact of loading all textures in the pool.
// It might be removed in the future, if we can mitigate the performance impact.
return false;
}
return false;
}
return true;
private static bool IsHandleConstructionOperation(Instruction inst)
{
return inst is
Instruction.Add or
Instruction.Subtract or
Instruction.Multiply or
Instruction.MultiplyHighS32 or
Instruction.MultiplyHighU32 or
Instruction.BitwiseAnd or
Instruction.BitwiseExclusiveOr or
Instruction.BitwiseNot or
Instruction.BitwiseOr or
Instruction.BitfieldExtractS32 or
Instruction.BitfieldExtractU32 or
Instruction.BitfieldInsert or
Instruction.ShiftLeft or
Instruction.ShiftRightS32 or
Instruction.ShiftRightU32 or
Instruction.MinimumU32 or
Instruction.MaximumU32 or
Instruction.ClampU32 or
Instruction.ConditionalSelect or
Instruction.Copy or
Instruction.VectorExtract;
}
private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp)
@@ -32,6 +32,29 @@ namespace Ryujinx.Graphics.Vulkan
public bool HasDepthStencil { get; private set; }
public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0);
public bool SetVirtualSize(uint width, uint height, uint layers)
{
if (AttachmentsCount != 0)
{
return false;
}
width = Math.Max(1u, width);
height = Math.Max(1u, height);
layers = Math.Max(1u, layers);
if (Width == width && Height == height && Layers == layers)
{
return false;
}
Width = width;
Height = height;
Layers = layers;
return true;
}
public FramebufferParams(Device device, TextureView view, uint width, uint height)
{
Format format = view.Info.Format;
@@ -843,6 +843,11 @@ namespace Ryujinx.Graphics.Vulkan
public void SetImage(ShaderStage stage, int binding, ITexture image)
{
_descriptorSetUpdater.SetImage(Cbs, stage, binding, image);
if (stage == ShaderStage.Fragment && image is TextureView view)
{
FramebufferParams?.SetVirtualSize((uint)view.Width, (uint)view.Height, (uint)view.Layers);
}
}
public void SetImage(int binding, Auto<DisposableImageView> image)
@@ -1607,6 +1612,24 @@ namespace Ryujinx.Graphics.Vulkan
private bool RecreateGraphicsPipelineIfNeeded()
{
if (FramebufferParams != null &&
FramebufferParams.AttachmentsCount == 0 &&
FramebufferParams.Width == 1 &&
FramebufferParams.Height == 1 &&
DynamicState.ViewportsCount != 0)
{
// An attachmentless fragment pass can reach its first draw before the storage
// image descriptor is rebound. At that point the null framebuffer still has
// its constructor fallback of 1x1, even though the guest viewport already
// describes the real render area. Seed the virtual framebuffer from that
// viewport so the first storage-image draw is not clipped to one pixel.
Silk.NET.Vulkan.Viewport viewport = DynamicState.Viewports[0];
uint width = (uint)Math.Max(1f, Math.Abs(viewport.Width));
uint height = (uint)Math.Max(1f, Math.Abs(viewport.Height));
FramebufferParams.SetVirtualSize(width, height, 1);
}
if (AutoFlush.ShouldFlushDraw(DrawCount))
{
Gd.FlushAllCommands();
+3
View File
@@ -36,6 +36,9 @@ namespace Ryujinx.Graphics.Vulkan
[GeneratedRegex(@"Adreno (\(TM\) )?(8[0-9]\d)")]
public static partial Regex Adreno8xxRegex();
[GeneratedRegex(@"^\d+\.\d+\.\d+\.\d+$")]
internal static partial Regex IntelWindowsDriverVersionRegex();
public static Vendor FromId(uint id)
{
return id switch
+62 -3
View File
@@ -115,6 +115,7 @@ namespace Ryujinx.Graphics.Vulkan
public string GpuDriver { get; private set; }
public string GpuRenderer { get; private set; }
public string GpuVersion { get; private set; }
public string GpuDriverVersion { get; private set; }
public bool PreferThreading => true;
@@ -185,6 +186,14 @@ namespace Ryujinx.Graphics.Vulkan
SType = StructureType.PhysicalDeviceProperties2,
};
PhysicalDeviceIDProperties propertiesId = new()
{
SType = StructureType.PhysicalDeviceIDProperties,
PNext = properties2.PNext,
};
properties2.PNext = &propertiesId;
PhysicalDeviceSubgroupProperties propertiesSubgroup = new()
{
SType = StructureType.PhysicalDeviceSubgroupProperties,
@@ -374,13 +383,17 @@ namespace Ryujinx.Graphics.Vulkan
GpuVendor = VendorUtils.GetNameFromId(properties.VendorID);
GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ?
VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred.
GpuDriverVersion = TryGetIntelWindowsDriverVersionFromUuid(ref propertiesId, out string intelDriverVersion) ? intelDriverVersion :
hasDriverProperties ?
GetDriverInfo(ref driverProperties) ?? ParseDriverVersion(ref properties) :
ParseDriverVersion(ref properties);
fixed (byte* deviceName = properties.DeviceName)
{
GpuRenderer = Marshal.PtrToStringAnsi((nint)deviceName);
}
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}";
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}";
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer);
@@ -846,7 +859,7 @@ namespace Ryujinx.Graphics.Vulkan
public HardwareInfo GetHardwareInfo()
{
return new HardwareInfo(GpuVendor, GpuRenderer, GpuDriver);
return new HardwareInfo(GpuVendor, GpuRenderer, GpuDriver, GpuDriverVersion);
}
/// <summary>
@@ -896,9 +909,55 @@ namespace Ryujinx.Graphics.Vulkan
return $"{(driverVersionRaw >> 22) & 0x3FF}.{(driverVersionRaw >> 14) & 0xFF}.{(driverVersionRaw >> 6) & 0xFF}.{driverVersionRaw & 0x3F}";
}
// Intel's Windows Vulkan driver exposes the 101.xxxx build portion in a custom layout.
if (properties.VendorID == 0x8086 && OperatingSystem.IsWindows())
{
return $"{driverVersionRaw >> 14}.{driverVersionRaw & 0x3FFF}";
}
return ParseStandardVulkanVersion(driverVersionRaw);
}
private static unsafe string GetDriverInfo(ref PhysicalDeviceDriverPropertiesKHR driverProperties)
{
fixed (byte* driverInfo = driverProperties.DriverInfo)
{
string driverInfoString = Marshal.PtrToStringAnsi((nint)driverInfo);
return string.IsNullOrWhiteSpace(driverInfoString) ? null : driverInfoString;
}
}
private static unsafe bool TryGetIntelWindowsDriverVersionFromUuid(ref PhysicalDeviceIDProperties propertiesId, out string driverVersion)
{
driverVersion = null;
if (!OperatingSystem.IsWindows())
{
return false;
}
fixed (byte* driverUuid = propertiesId.DriverUuid)
{
string driverUuidString = Marshal.PtrToStringAnsi((nint)driverUuid, (int)Vk.UuidSize);
int terminatorIndex = driverUuidString.IndexOf('\0');
if (terminatorIndex >= 0)
{
driverUuidString = driverUuidString[..terminatorIndex];
}
if (VendorUtils.IntelWindowsDriverVersionRegex().IsMatch(driverUuidString))
{
driverVersion = driverUuidString;
return true;
}
}
return false;
}
internal PrimitiveTopology TopologyRemap(PrimitiveTopology topology)
{
return topology switch
@@ -924,7 +983,7 @@ namespace Ryujinx.Graphics.Vulkan
private void PrintGpuInformation()
{
Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})");
Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion}, Driver: {GpuDriver} {GpuDriverVersion})");
Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB");
}
+6 -9
View File
@@ -1,4 +1,5 @@
using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel.Process;
@@ -9,12 +10,10 @@ namespace Ryujinx.HLE.HOS
interface IArmProcessContext : IProcessContext
{
IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
PtcCacheInfo cacheInfo,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize,
string cacheSelector);
ulong codeSize);
}
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
@@ -67,15 +66,13 @@ namespace Ryujinx.HLE.HOS
}
public IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
PtcCacheInfo cacheInfo,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize,
string cacheSelector)
ulong codeSize)
{
_cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector);
return _cpuContext.LoadDiskCache(cacheInfo, diskCacheEnabled);
}
public void InvalidateCacheRegion(ulong address, ulong size)
@@ -1,3 +1,4 @@
using ARMeilleure.Translation.PTC;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
@@ -8,6 +9,7 @@ using Ryujinx.Cpu.Nce;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Memory;
using System;
using System.Runtime.InteropServices;
@@ -18,8 +20,10 @@ namespace Ryujinx.HLE.HOS
{
private readonly ITickSource _tickSource;
private readonly GpuContext _gpu;
private readonly string _titleIdText;
private readonly ulong _programId;
private readonly byte _programIndex;
private readonly string _displayVersion;
private readonly ProcessKind _processKind;
private readonly bool _diskCacheEnabled;
private readonly string _diskCacheSelector;
private readonly ulong _codeAddress;
@@ -30,8 +34,10 @@ namespace Ryujinx.HLE.HOS
public ArmProcessContextFactory(
ITickSource tickSource,
GpuContext gpu,
string titleIdText,
ulong programId,
byte programIndex,
string displayVersion,
ProcessKind processKind,
bool diskCacheEnabled,
string diskCacheSelector,
ulong codeAddress,
@@ -39,8 +45,10 @@ namespace Ryujinx.HLE.HOS
{
_tickSource = tickSource;
_gpu = gpu;
_titleIdText = titleIdText;
_programId = programId;
_programIndex = programIndex;
_displayVersion = displayVersion;
_processKind = processKind;
_diskCacheEnabled = diskCacheEnabled;
_diskCacheSelector = diskCacheSelector;
_codeAddress = codeAddress;
@@ -160,8 +168,18 @@ namespace Ryujinx.HLE.HOS
}
string cacheSelector = _diskCacheSelector ?? "default";
string programIdText = _programId == 0 ? string.Empty : $"{_programId:x16}";
string applicationIdText = _programId == 0 ? string.Empty : $"{_programId & ~0xFul:x16}";
PtcCacheInfo cacheInfo = new(
pid,
programIdText,
applicationIdText,
_programIndex,
_displayVersion,
_processKind.ToString(),
cacheSelector);
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, cacheSelector);
DiskCacheLoadState = processContext.Initialize(cacheInfo, _diskCacheEnabled, _codeAddress, _codeSize);
return processContext;
}
@@ -12,12 +12,23 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
{
class ICommonStateGetter : DisposableIpcService
{
// Nintendo serves the VR goggle commands based on Title ID, apparently.
private static ReadOnlySpan<ulong> _supportedVRGoggleTitles =>
[
0x0100CA001D972000, // Nintendo Classics: Virtual Boy NSO
0x0100BFC01D976000 // Nintendo Classics: Virtual Boy NSO
];
private int _resolutionWidth = 1280;
private int _resolutionHeight = 720;
private readonly ServiceCtx _context;
private readonly Apm.ManagerServer _apmManagerServer;
private readonly Apm.SystemManagerServer _apmSystemManagerServer;
private bool _vrModeEnabled;
private bool _vrMode3dEnabled;
#pragma warning disable CS0414, IDE0052 // Remove unread private member
private bool _lcdBacklighOffEnabled;
private bool _requestExitToLibraryAppletAtExecuteNextProgramEnabled;
@@ -153,6 +164,47 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[CommandCmif(30)]
// GetHomeButtonReaderLockAccessor() -> nn::am::service::ILockAccessor
public ResultCode GetHomeButtonReaderLockAccessor(ServiceCtx context)
{
// We currently do not have any home button functionality, so it's fine to stub this for now.
// Similar to using GetReaderLockAccessorEx() with inval=0.
Logger.Stub?.PrintStub(LogClass.ServiceAm);
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(31)] // 2.0.0 +
// GetReaderLockAccessorEx(uint unknown) -> nn::am::service::ILockAccessor
public ResultCode GetReaderLockAccessorEx(ServiceCtx context)
{
uint unknown = context.RequestData.ReadUInt32();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
if (unknown < 0 || unknown > 3)
{
throw new ArgumentOutOfRangeException(nameof(unknown));
}
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(32)] // 2.0.0+
// GetWriterLockAccessorEx(uint unknown) -> nn::am::service::ILockAccessor
public ResultCode GetWriterLockAccessorEx(ServiceCtx context)
{
uint unknown = context.RequestData.ReadUInt32();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
if (unknown < 0 || unknown > 3)
{
throw new ArgumentOutOfRangeException(nameof(unknown));
}
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(50)] // 3.0.0+
// IsVrModeEnabled() -> b8
public ResultCode IsVrModeEnabled(ServiceCtx context)
@@ -177,8 +229,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
// SetLcdBacklighOffEnabled(b8)
public ResultCode SetLcdBacklighOffEnabled(ServiceCtx context)
{
// NOTE: Service sets a private field here, maybe this field is used somewhere else to turned off the backlight.
// Since we don't support backlight, it's fine to do nothing.
// NOTE: Service sets a private field here, maybe this field is used somewhere else to turn off the backlight.
// Since we don't support the backlight feature, it's fine to stub it.
_lcdBacklighOffEnabled = context.RequestData.ReadBoolean();
@@ -298,18 +350,43 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return (ResultCode)_apmSystemManagerServer.GetCurrentPerformanceConfiguration(context);
}
[CommandCmif(130)] // 21.0.0+
// EnableStartupLogoDisappearedMessage()
public ResultCode EnableStartupLogoDisappearedMessage(ServiceCtx context)
{
// NOTE: Service only delivers the message once the startup logo has actually
// disappeared. We never display one, so it is already deliverable. Callers
// gate their first frame on this message and hang without it.
AppletStateMgr appletState = context.Device.System.AppletState;
appletState.Messages.Enqueue(AppletMessage.StartupLogoDisappeared);
appletState.MessageEvent.ReadableEvent.Signal();
return ResultCode.Success;
}
[CommandCmif(300)] // 9.0.0+
// GetSettingsPlatformRegion() -> u8
public ResultCode GetSettingsPlatformRegion(ServiceCtx context)
{
PlatformRegion platformRegion = context.Device.System.State.DesiredRegionCode == (uint)RegionCode.China ? PlatformRegion.China : PlatformRegion.Global;
// FIXME: Call set:sys GetPlatformRegion
context.ResponseData.Write((byte)platformRegion);
return ResultCode.Success;
}
[CommandCmif(610)] // 21.0.0+
// UnknownCommand610(long unknown)
public ResultCode UnknownCommand610(ServiceCtx context)
{
long unknown = context.RequestData.ReadInt64();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
return ResultCode.Success;
}
[CommandCmif(900)] // 11.0.0+
// SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled()
public ResultCode SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled(ServiceCtx context)
@@ -320,6 +397,100 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[CommandCmif(1000)] // 19.0.0+
// BeginVrMode3d()
public ResultCode BeginVrMode3d(ServiceCtx context)
{
// NOTE: Service also applies a stereo scale to the goggle display, which we don't
// model, so only the state is kept here.
_vrMode3dEnabled = true;
return ResultCode.Success;
}
[CommandCmif(1001)] // 19.0.0+
// EndVrMode3d()
public ResultCode EndVrMode3d(ServiceCtx context)
{
_vrMode3dEnabled = false;
return ResultCode.Success;
}
[CommandCmif(1002)] // 19.0.0+
// IsVrModeEnabled3d() -> b8
public ResultCode IsVrModeEnabled3d(ServiceCtx context)
{
context.ResponseData.Write(_vrMode3dEnabled);
return ResultCode.Success;
}
[CommandCmif(1003)] // 21.0.0+
// GetVrLaboGoggleViewport() -> (s32 x, s32 y, s32 width, s32 height)
public ResultCode GetVrLaboGoggleViewport(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
int VrDisplayCoordinateX = 0;
int VrDisplayCoordinateY = 0;
int VrDisplayWidth = _resolutionWidth;
int VrDisplayHeight = _resolutionHeight;
context.ResponseData.Write(VrDisplayCoordinateX);
context.ResponseData.Write(VrDisplayCoordinateY);
context.ResponseData.Write(VrDisplayWidth);
context.ResponseData.Write(VrDisplayHeight);
return ResultCode.Success;
}
[CommandCmif(1004)] // 21.0.0+
// GetPanelPhysicalSizeForSpecificTitle() -> (f32 width, f32 height)
public ResultCode GetPanelPhysicalSizeForSpecificTitle(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
// The switch provides micrometres, but the command reports millimetres.
// 6.2 inch 16:9 panel.
float PanelPhysicalWidthMicroMeters = 137250f;
float PanelPhysicalHeightMicroMeters = 77200f;
context.ResponseData.Write(PanelPhysicalWidthMicroMeters / 1000f);
context.ResponseData.Write(PanelPhysicalHeightMicroMeters / 1000f);
return ResultCode.Success;
}
[CommandCmif(1005)] // 21.0.0+
// GetPanelResolutionForSpecificTitle() -> (s32 width, s32 height)
public ResultCode GetPanelResolutionForSpecificTitle(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
context.ResponseData.Write(_resolutionWidth);
context.ResponseData.Write(_resolutionHeight);
return ResultCode.Success;
}
private static bool IsVrLaboGoggleSupportedTitle(ServiceCtx context)
{
ulong programId = context.Device.Processes.ActiveApplication.ProgramId;
return _supportedVRGoggleTitles.Contains(programId);
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
@@ -0,0 +1,80 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy
{
class ILockAccessor : IpcService
{
private readonly ServiceCtx _context;
private readonly Apm.ManagerServer _apmManagerServer;
private readonly Apm.SystemManagerServer _apmSystemManagerServer;
private readonly KEvent _lockEvent;
private int _lockEventHandle;
public ILockAccessor(ServiceCtx context)
{
_context = context;
_apmManagerServer = new Apm.ManagerServer(context);
_apmSystemManagerServer = new Apm.SystemManagerServer(context);
_lockEvent = new KEvent(context.Device.System.KernelContext);
}
[CommandCmif(1)]
// TryLock(unknown u8 bool) -> unknown u8 bool
public ResultCode TryLock(ServiceCtx context)
{
// Official sw only uses inflag=false.
// Official sw just closes the output handle.
// The input flag controls whether this returns the output handle.
bool unknown = context.RequestData.ReadBoolean();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
context.ResponseData.Write(false);
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_lockEventHandle);
return ResultCode.Success;
}
[CommandCmif(2)]
// Unlock()
public ResultCode Unlock(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(3)]
// GetEvent() -> EventHandle w/ autoclear = false
public ResultCode GetEvent(ServiceCtx context)
{
if (_lockEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_lockEvent.ReadableEvent, out _lockEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_lockEventHandle);
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(4)] // 10.0.0+
// IsLocked() -> unknown u8 bool
public ResultCode IsLocked(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
context.ResponseData.Write(false);
return ResultCode.Success;
}
}
}
@@ -32,5 +32,6 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
DetectShortPressingCaptureButton = 90,
AlbumScreenShotTaken = 92,
AlbumRecordingSaved = 93,
StartupLogoDisappeared = 95, // 21.0.0+
}
}
@@ -0,0 +1,23 @@
namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad
{
struct NpadCondition
{
#pragma warning disable CS0414 // Field is assigned but its value is never used
private uint _00;
private uint _04;
private NpadJoyHoldType _holdType;
private uint _0C;
#pragma warning restore CS0414 // Field is assigned but its value is never used
public static NpadCondition Create()
{
return new NpadCondition()
{
_00 = 0,
_04 = 1,
_holdType = NpadJoyHoldType.Horizontal,
_0C = 1,
};
}
}
}
@@ -46,6 +46,12 @@ namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory
[FieldOffset(0x9A00)]
public Array10<NpadState> Npads;
/// <summary>
/// Pad Condition.
/// </summary>
[FieldOffset(0x3e200)]
public NpadCondition Condition;
public static SharedMemory Create()
{
SharedMemory result = new()
@@ -54,6 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory
TouchScreen = RingLifo<TouchScreenState>.Create(),
Mouse = RingLifo<MouseState>.Create(),
Keyboard = RingLifo<KeyboardState>.Create(),
Condition = NpadCondition.Create(),
};
Span<NpadState> npadsSpan = result.Npads.AsSpan();
@@ -183,8 +183,10 @@ namespace Ryujinx.HLE.Loaders.Processes
ArmProcessContextFactory processContextFactory = new(
context.Device.System.TickSource,
context.Device.Gpu,
string.Empty,
string.Empty,
kip.ProgramId,
0,
kip.Version.ToString(),
ProcessResult.GetProcessKind(kip.ProgramId),
false,
null,
codeAddress,
@@ -392,8 +394,10 @@ namespace Ryujinx.HLE.Loaders.Processes
ArmProcessContextFactory processContextFactory = new(
context.Device.System.TickSource,
context.Device.Gpu,
$"{programId:x16}",
programId,
programIndex,
displayVersion,
ProcessResult.GetProcessKind(programId),
diskCacheEnabled,
diskCacheSelector,
codeStart,
@@ -87,7 +87,7 @@ namespace Ryujinx.HLE.Loaders.Processes
AllowCodeMemoryForJit = allowCodeMemoryForJit;
}
private static ProcessKind GetProcessKind(ulong programId)
internal static ProcessKind GetProcessKind(ulong programId)
{
if (programId == 0)
{
+20 -4
View File
@@ -43,6 +43,7 @@ using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan;
using SkiaSharp;
using SPB.Graphics.Exceptions;
using SPB.Graphics.Vulkan;
using System;
using System.Collections.Generic;
@@ -640,8 +641,22 @@ namespace Ryujinx.Ava
if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow)
{
// Try to bind the OpenGL context before calling the shutdown event.
openGlWindow.MakeCurrent(false, false);
try
{
// Try to bind the OpenGL context before disposing GPU resources.
openGlWindow.MakeCurrent();
}
catch (ContextException e) when (_userChannelPersistence.ShouldRestart)
{
// ExecuteProgram may detach the old native window before GPU
// disposal. Allow the old context to be released with the window
// and continue the requested program relaunch.
Logger.Warning?.Print(
LogClass.UI,
$"Failed to bind OpenGL context during program relaunch: {e}");
return;
}
Device.DisposeGpu();
@@ -1051,9 +1066,10 @@ namespace Ryujinx.Ava
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_viewModel.StartGamesInFullscreen)
if (_viewModel.StartGamesInFullscreen && _viewModel.WindowState is not WindowState.FullScreen)
{
_viewModel.WindowState = WindowState.FullScreen;
// Use the view model toggle so decoration ordering matches user toggles.
_viewModel.ToggleFullscreen();
}
if (_viewModel.WindowState == WindowState.FullScreen || _viewModel.StartGamesWithoutUI)