Add raw copy dependencies for incompatible textures

Trails in the Sky 1st uses texture descriptors with different formats and dimensions that map to the same region of guest GPU memory. For example, the game interprets the same underlying data as both an R32Uint 2x1 texture and an R32G32Float 1x1 texture. Both textures have an 8-byte logical payload, but their texel formats and dimensions are different.

Ryubing cannot represent these two descriptors as normally compatible texture views, so it creates separate host texture objects for them. Since these host textures represent the same guest memory, a modification made to one of them should become visible to the other before the other texture is read. However, the existing synchronization mechanism does not fully handle this situation.

The game uses this pattern for its exposure or brightness history. A compute shader first writes the updated exposure data to the R32G32Float 1x1 host texture. A later operation then reads the raw bits of the same data through the R32Uint 2x1 host texture. The original Ryubing implementation did not synchronize the contents of these two separate host textures. As a result, the reading texture received stale zero values, which caused the shader to calculate an incorrectly low exposure value and made the rendered image appear too dark.

TextureGroup already has a guest-memory synchronization mechanism for incompatible overlaps. When _flushIncompatibleOverlaps is enabled, the most recent texture contents are written back to guest memory, after which the other texture reloads the data. However, this mechanism was primarily enabled through IsFormatHostIncompatible, which checks whether an individual texture format can be correctly supported by the host GPU.

In this case, both R32Uint and R32G32Float are individually supported by the host GPU. Therefore, IsFormatHostIncompatible does not enable this synchronization path. The issue is not that either format is unsupported. The issue is that two individually supported aliases, represented by separate host textures, do not maintain coherent contents.

In theory, the latest contents could be flushed to guest memory immediately after every GPU write to a host texture, allowing other textures that map to the same guest memory to reload the updated data. However, doing so could frequently trigger expensive operations such as GPU waits, data readbacks, guest texture layout conversions, and memory-tracking updates. This would be particularly costly for textures that are updated every frame or during every compute dispatch.

For this reason, the fix does not require an immediate flush after every write. Instead, it extends Ryubing's existing TextureDependency and TextureGroupHandle mechanisms. When two fully incompatible textures map to exactly the same guest memory and satisfy a strict set of raw-copy requirements, the program creates a raw-copy dependency between them.

When the source texture is modified, the target texture is only marked as requiring a raw copy. The latest logical raw bytes are copied from the source only when the target texture is actually about to be used. This keeps the contents of the separate host texture aliases coherent while avoiding unnecessary round trips through guest memory.
This commit is contained in:
avan
2026-08-12 13:57:33 -05:00
committed by KeatonTheBot
parent b0eb271b5b
commit a4412346a7
3 changed files with 170 additions and 47 deletions
@@ -17,21 +17,30 @@ namespace Ryujinx.Graphics.Gpu.Image
public TextureDependency Other;
/// <summary>
/// Create a new texture dependency.
/// Indicates whether this dependency requires an exact raw byte copy
/// instead of a regular texture copy.
/// </summary>
public readonly bool RawCopy;
/// <summary>
/// Creates a new texture dependency.
/// </summary>
/// <param name="handle">The handle that owns the dependency</param>
public TextureDependency(TextureGroupHandle handle)
/// <param name="rawCopy">True if copies performed for this dependency must preserve the exact raw bytes; false to use a regular texture copy</param>
public TextureDependency(TextureGroupHandle handle, bool rawCopy = false)
{
Handle = handle;
RawCopy = rawCopy;
}
/// <summary>
/// Signal that the owner of this dependency has been modified,
/// meaning that the other dependency's handle must defer a copy from it.
/// Signals that the owner of this dependency has been modified,
/// causing the other dependency's handle to defer a copy from it.
/// The dependency's copy mode is propagated to the deferred copy.
/// </summary>
public void SignalModified()
{
Other.Handle.DeferCopy(Handle);
Other.Handle.DeferCopy(Handle, RawCopy);
}
}
}
+81 -18
View File
@@ -175,16 +175,16 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Initialize all incompatible overlaps in the list, registering them with the other texture groups
/// and creating copy dependencies when partially compatible.
/// Initializes all incompatible overlaps in the list, registers them with the other texture groups,
/// and creates regular or exact raw byte copy dependencies where supported.
/// </summary>
public void InitializeOverlaps()
{
foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps)
{
if (overlap.Compatibility == TextureViewCompatibility.LayoutIncompatible)
if (overlap.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
{
CreateCopyDependency(overlap.Group, false);
CreateCopyDependency(overlap.Group, false, overlap.Compatibility);
}
overlap.Group._incompatibleOverlaps.Add(new TextureIncompatibleOverlap(this, overlap.Compatibility));
@@ -1532,13 +1532,57 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Creates a copy dependency to another texture group, where handles overlap.
/// Scans through all handles to find compatible patches in the other group.
/// Determines whether an exact raw byte copy dependency can be safely created
/// between this texture group and another fully incompatible texture group.
/// </summary>
/// <param name="other">The overlapping texture group to evaluate</param>
/// <returns>
/// True if both texture groups describe the same contiguous guest memory range
/// and have compatible logical payload sizes and resource properties for an exact raw byte copy;
/// otherwise, false
/// </returns>
private bool CanCreateRawCopyDependency(TextureGroup other)
{
TextureInfo info = Storage.Info;
TextureInfo otherInfo = other.Storage.Info;
long size = (long)info.Width * info.Height * info.GetDepth() * info.FormatInfo.BytesPerPixel;
long otherSize = (long)otherInfo.Width * otherInfo.Height * otherInfo.GetDepth() * otherInfo.FormatInfo.BytesPerPixel;
return size > 0 &&
size <= int.MaxValue &&
size == otherSize &&
Storage.Range.Count == 1 &&
other.Storage.Range.Count == 1 &&
Storage.Range.GetSubRange(0).Address == other.Storage.Range.GetSubRange(0).Address &&
Storage.Range.GetSubRange(0).Size == other.Storage.Range.GetSubRange(0).Size &&
info.Levels == 1 &&
otherInfo.Levels == 1 &&
info.GetSlices() == 1 &&
otherInfo.GetSlices() == 1 &&
info.Samples == 1 &&
otherInfo.Samples == 1 &&
info.Target != Target.TextureBuffer &&
otherInfo.Target != Target.TextureBuffer &&
!info.FormatInfo.IsCompressed &&
!otherInfo.FormatInfo.IsCompressed &&
!info.FormatInfo.Format.IsDepthOrStencil() &&
!otherInfo.FormatInfo.Format.IsDepthOrStencil();
}
/// <summary>
/// Creates copy dependencies between overlapping handles in this texture group
/// and another texture group.
/// Regular texture copy dependencies are created for compatible handle pairs.
/// For fully incompatible groups, an exact raw byte copy dependency may be created
/// when the strict raw copy requirements are satisfied.
/// </summary>
/// <param name="other">The texture group that overlaps this one</param>
/// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
public void CreateCopyDependency(TextureGroup other, bool copyTo)
/// <param name="copyTo">True if this texture should initially be copied to the other texture; false if the other texture should initially be copied to this texture</param>
/// <param name="compatibility">The view compatibility between the two texture groups, used to determine whether a regular texture copy or an exact raw byte copy may be used</param>
public void CreateCopyDependency(TextureGroup other, bool copyTo,
TextureViewCompatibility compatibility = TextureViewCompatibility.LayoutIncompatible)
{
bool rawCopy = compatibility == TextureViewCompatibility.Incompatible && CanCreateRawCopyDependency(other);
for (int i = 0; i < _allOffsets.Length; i++)
{
(_, int level) = GetLayerLevelForView(i);
@@ -1557,8 +1601,10 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo info = Storage.Info;
TextureInfo otherInfo = other.Storage.Info;
if (TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel))
bool textureCopy = TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel);
if (textureCopy || rawCopy)
{
// These textures are copy compatible. Create the dependency.
@@ -1568,18 +1614,34 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureGroupHandle handle = _handles[i];
TextureGroupHandle otherHandle = other._handles[j];
handle.CreateCopyDependency(otherHandle, copyTo);
handle.CreateCopyDependency(otherHandle, copyTo, rawCopy);
// If "copyTo" is true, this texture must copy to the other.
// Otherwise, it must copy to this texture.
if (copyTo)
{
otherHandle.Copy(_context, handle);
if (rawCopy)
{
otherHandle.DeferCopy(handle, true);
otherHandle.Copy(_context);
}
else
{
otherHandle.Copy(_context, handle);
}
}
else
{
handle.Copy(_context, otherHandle);
if (rawCopy)
{
handle.DeferCopy(otherHandle, true);
handle.Copy(_context);
}
else
{
handle.Copy(_context, otherHandle);
}
}
}
}
@@ -1588,18 +1650,19 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Registers another texture group as an incompatible overlap, if not already registered.
/// Registers another texture group as an incompatible overlap, if it has not already been registered,
/// and creates a supported copy dependency when requested.
/// </summary>
/// <param name="other">The texture group to add to the incompatible overlaps list</param>
/// <param name="copy">True if the overlap should register copy dependencies</param>
/// <param name="other">The incompatible texture overlap to register</param>
/// <param name="copy">True to attempt to create a regular or exact raw byte copy dependency for the overlapping texture groups; otherwise, false</param>
public void RegisterIncompatibleOverlap(TextureIncompatibleOverlap other, bool copy)
{
if (!_incompatibleOverlaps.Any(overlap => overlap.Group == other.Group))
{
if (copy && other.Compatibility == TextureViewCompatibility.LayoutIncompatible)
if (copy && other.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
{
// Any of the group's views may share compatibility, even if the parents do not fully.
CreateCopyDependency(other.Group, false);
CreateCopyDependency(other.Group, false, other.Compatibility);
}
_incompatibleOverlaps.Add(other);
@@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Synchronization;
using Ryujinx.Memory.Tracking;
using System;
@@ -106,6 +108,12 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public TextureGroupHandle DeferredCopy { get; set; }
/// <summary>
/// Indicates whether the pending deferred copy must preserve the exact raw bytes
/// instead of using a regular texture copy.
/// </summary>
public bool DeferredCopyRaw { get; set; }
/// <summary>
/// Create a new texture group handle, representing a range of views in a storage texture.
/// </summary>
@@ -160,8 +168,9 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// The action to perform when a memory tracking handle is flipped to dirty.
/// This notifies overlapping textures that the memory needs to be synchronized.
/// Handles a memory tracking region becoming dirty.
/// This notifies all overlapping textures that their memory must be synchronized
/// and discards any pending deferred copy state.
/// </summary>
private void DirtyAction()
{
@@ -177,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
DeferredCopy = null;
ClearDeferredCopy();
}
/// <summary>
@@ -186,7 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public void DiscardData()
{
DeferredCopy = null;
ClearDeferredCopy();
foreach (RegionHandle handle in Handles)
{
@@ -469,14 +478,22 @@ namespace Ryujinx.Graphics.Gpu.Image
return syncpoint || !lastInBuffer;
}
private void ClearDeferredCopy()
{
DeferredCopy = null;
DeferredCopyRaw = false;
}
/// <summary>
/// Signal that a copy dependent texture has been modified, and must have its data copied to this one.
/// Defers a copy from another texture group handle until this handle is next synchronized.
/// </summary>
/// <param name="copyFrom">The texture handle that must defer a copy to this one</param>
public void DeferCopy(TextureGroupHandle copyFrom)
/// <param name="copyFrom">The texture group handle containing the source data</param>
/// <param name="rawCopy">True if the deferred copy must preserve the exact raw bytes; false to use a regular texture copy</param>
public void DeferCopy(TextureGroupHandle copyFrom, bool rawCopy = false)
{
Modified = false;
DeferredCopy = copyFrom;
DeferredCopyRaw = rawCopy;
_group.Storage.SignalGroupDirty();
@@ -487,11 +504,12 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Create a copy dependency between this handle, and another.
/// Creates a two-way copy dependency between this handle and another handle.
/// </summary>
/// <param name="other">The handle to create a copy dependency to</param>
/// <param name="copyToOther">True if a copy should be deferred to all of the other handle's dependencies</param>
public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false)
/// <param name="other">The other handle participating in the dependency</param>
/// <param name="copyToOther">True if a pending copy from this handle should also be propagated to the other handle's existing dependencies; otherwise, false</param>
/// <param name="rawCopy">True if the dependency must use exact raw byte copies; false to use regular texture copies</param>
public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false, bool rawCopy = false)
{
// Does this dependency already exist?
foreach (TextureDependency existing in Dependencies)
@@ -506,8 +524,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_group.HasCopyDependencies = true;
other._group.HasCopyDependencies = true;
TextureDependency dependency = new(this);
TextureDependency otherDependency = new(other);
TextureDependency dependency = new(this, rawCopy);
TextureDependency otherDependency = new(other, rawCopy);
dependency.Other = otherDependency;
otherDependency.Other = dependency;
@@ -515,6 +533,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Dependencies.Add(dependency);
other.Dependencies.Add(otherDependency);
if (rawCopy)
{
return;
}
// Recursively create dependency:
// All of this handle's dependencies must depend on the other.
foreach (TextureDependency existing in Dependencies.ToArray())
@@ -559,19 +582,24 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided.
/// Copies texture data from the provided handle to this handle,
/// or fulfills the pending deferred copy when no source handle is provided.
/// Depending on the dependency mode, the operation uses either a regular texture copy
/// or an exact raw byte copy.
/// </summary>
/// <param name="context">GPU context to register sync for modified handles</param>
/// <param name="fromHandle">The handle to copy from. If not provided, this method will copy from and clear the deferred copy instead</param>
/// <returns>True if the copy was performed, false otherwise</returns>
/// <param name="context">The GPU context used to register synchronization for modified handles</param>
/// <param name="fromHandle">The handle to copy from, or null to use and acknowledge the pending deferred copy</param>
/// <returns>True if the copy was performed; otherwise, false</returns>
public bool Copy(GpuContext context, TextureGroupHandle fromHandle = null)
{
bool result = false;
bool shouldCopy = false;
bool rawCopy = false;
if (fromHandle == null)
{
fromHandle = DeferredCopy;
rawCopy = DeferredCopyRaw;
if (fromHandle != null)
{
@@ -584,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (fromHandle._bindCount == 0)
{
// Repeat the copy in future if the bind count is greater than 0.
DeferredCopy = null;
ClearDeferredCopy();
}
}
}
@@ -606,12 +634,35 @@ namespace Ryujinx.Graphics.Gpu.Image
to.PropagateScale(from);
}
from.HostTexture.CopyTo(
to.HostTexture,
fromHandle._firstLayer,
_firstLayer,
fromHandle._firstLevel,
_firstLevel);
if (rawCopy)
{
PinnedSpan<byte> pinned = from.HostTexture.GetData(fromHandle._firstLayer, fromHandle._firstLevel);
try
{
ReadOnlySpan<byte> data = pinned.Get();
long targetSize = (long)to.Width * to.Height * to.Info.GetDepth() * to.Info.FormatInfo.BytesPerPixel;
if (targetSize <= 0 || targetSize > int.MaxValue || data.Length != targetSize)
{
return false;
}
to.HostTexture.SetData(MemoryOwner<byte>.RentCopy(data), _firstLayer, _firstLevel);
}
finally
{
pinned.Dispose();
}
}
else
{
from.HostTexture.CopyTo(
to.HostTexture,
fromHandle._firstLayer,
_firstLayer,
fromHandle._firstLevel,
_firstLevel);
}
if (fromHandle.Modified)
{