VP9-improvements (#132)

- Improves MMNV request handling by tracking active clock requests per session and returning the effective requested rate for each module.
- Treats sentinel clock requests (`0` / negative values) as request clears.
- Preserves VP9 segment maps across inter frames, resetting them for key/intra/size-change cases.
- Avoids stale VP9 motion-vector state by disabling previous MV lookup on key frames and clearing output MVs for intra-only frames.

This fixes VP9 playback corruption seen in several Just Dance videos where stale segmentation/MV state could leak across frames and produce blocky/discolored output until a later refresh frame.
The MMNV changes also make the service behavior closer to what games expect when requesting and clearing NvDec clocks, removing the stub spam for this service.

Co-authored-by: MrKev312 <34964788+MrKev312@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/132
Reviewed-by: sh0inx <sh0inx@noreply.git.ryujinx.app>
This commit is contained in:
MrKev
2026-07-06 14:23:35 +00:00
committed by sh0inx
parent e47417a161
commit 68432d210b
6 changed files with 175 additions and 36 deletions

View File

@@ -11,6 +11,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
public bool IsHardwareAccelerated => false; public bool IsHardwareAccelerated => false;
private readonly MemoryAllocator _allocator = new(); private readonly MemoryAllocator _allocator = new();
private byte[] _lastSegmentMap = [];
private int _lastSegmentMapMiRows;
private int _lastSegmentMapMiCols;
public ISurface CreateSurface(int width, int height) public ISurface CreateSurface(int width, int height)
{ {
@@ -106,7 +109,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
cm.AllocTileWorkerData(_allocator, tileCols, tileRows, maxThreads); cm.AllocTileWorkerData(_allocator, tileCols, tileRows, maxThreads);
cm.AllocContextBuffers(_allocator, output.Width, output.Height); cm.AllocContextBuffers(_allocator, output.Width, output.Height);
cm.InitContextBuffers(); cm.InitContextBuffers();
LoadSegmentMap(ref cm, ref pictureInfo);
cm.SetupSegmentationDequant(); cm.SetupSegmentationDequant();
cm.SetupScaleFactors(); cm.SetupScaleFactors();
@@ -153,6 +158,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
} }
} }
SaveSegmentMap(ref cm);
cm.GetMvs(mvsOut); cm.GetMvs(mvsOut);
cm.FreeTileWorkerData(_allocator); cm.FreeTileWorkerData(_allocator);
@@ -161,6 +167,52 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
return true; return true;
} }
private void LoadSegmentMap(ref Vp9Common cm, ref Vp9PictureInfo pictureInfo)
{
int mapLength = cm.MiRows * cm.MiCols;
if (mapLength == 0 || cm.LastFrameSegMap.IsNull)
{
return;
}
bool reset =
pictureInfo.IsKeyFrame ||
pictureInfo.IntraOnly ||
_lastSegmentMapMiRows != cm.MiRows ||
_lastSegmentMapMiCols != cm.MiCols;
if (_lastSegmentMap.Length != mapLength)
{
_lastSegmentMap = new byte[mapLength];
}
else if (reset)
{
Array.Clear(_lastSegmentMap);
}
_lastSegmentMapMiRows = cm.MiRows;
_lastSegmentMapMiCols = cm.MiCols;
_lastSegmentMap.CopyTo(cm.LastFrameSegMap.AsSpan());
}
private void SaveSegmentMap(ref Vp9Common cm)
{
int mapLength = cm.MiRows * cm.MiCols;
if (!cm.Seg.Enabled || mapLength == 0 || cm.CurrentFrameSegMap.IsNull)
{
return;
}
if (_lastSegmentMap.Length != mapLength)
{
_lastSegmentMap = new byte[mapLength];
}
cm.CurrentFrameSegMap.AsSpan()[..mapLength].CopyTo(_lastSegmentMap);
_lastSegmentMapMiRows = cm.MiRows;
_lastSegmentMapMiCols = cm.MiCols;
}
public void Dispose() public void Dispose()
{ {
_allocator.Dispose(); _allocator.Dispose();

View File

@@ -1088,6 +1088,12 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types
public void GetMvs(Span<Vp9MvRef> mvs) public void GetMvs(Span<Vp9MvRef> mvs)
{ {
if (FrameIsIntraOnly())
{
mvs.Clear();
return;
}
if (mvs.Length > CurFrameMvs.Length) if (mvs.Length > CurFrameMvs.Length)
{ {
throw new ArgumentException( throw new ArgumentException(

View File

@@ -53,6 +53,7 @@ namespace Ryujinx.Graphics.Nvdec.Types.Vp9
IsKeyFrame = Flags.HasFlag(FrameFlags.IsKeyFrame), IsKeyFrame = Flags.HasFlag(FrameFlags.IsKeyFrame),
IntraOnly = Flags.HasFlag(FrameFlags.IntraOnly), IntraOnly = Flags.HasFlag(FrameFlags.IntraOnly),
UsePrevInFindMvRefs = UsePrevInFindMvRefs =
!Flags.HasFlag(FrameFlags.IsKeyFrame) &&
!Flags.HasFlag(FrameFlags.ErrorResilientMode) && !Flags.HasFlag(FrameFlags.ErrorResilientMode) &&
!Flags.HasFlag(FrameFlags.FrameSizeChanged) && !Flags.HasFlag(FrameFlags.FrameSizeChanged) &&
!Flags.HasFlag(FrameFlags.IntraOnly) && !Flags.HasFlag(FrameFlags.IntraOnly) &&

View File

@@ -16,10 +16,9 @@ namespace Ryujinx.Horizon.MmNv.Ipc
public Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent) public Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent)
{ {
bool isAutoClearEvent = autoClearEvent != 0; bool isAutoClearEvent = autoClearEvent != 0;
uint requestId = Register(module, fgmPriority, isAutoClearEvent);
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent }); Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
Register(module, fgmPriority, isAutoClearEvent);
return Result.Success; return Result.Success;
} }
@@ -27,41 +26,48 @@ namespace Ryujinx.Horizon.MmNv.Ipc
[CmifCommand(1)] [CmifCommand(1)]
public Result FinalizeOld(Module module) public Result FinalizeOld(Module module)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
lock (_sessionList) lock (_sessionList)
{ {
_sessionList.Remove(GetSessionByModule(module)); Session session = GetSessionByModule(module);
if (session != null)
{
_sessionList.Remove(session);
}
} }
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized module={module}");
return Result.Success; return Result.Success;
} }
[CmifCommand(2)] [CmifCommand(2)]
public Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax) public Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, clockRateMin, clockRateMax }); uint actualClockRate;
lock (_sessionList) lock (_sessionList)
{ {
GetSessionByModule(module)?.SetAndWait(clockRateMin, clockRateMax); Session session = GetSessionByModule(module) ?? RegisterSession(module, false);
ApplyClockRequest(session, clockRateMin, timeout);
actualClockRate = GetEffectiveClockRate(module);
} }
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait module={module} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
return Result.Success; return Result.Success;
} }
[CmifCommand(3)] [CmifCommand(3)]
public Result GetOld(out uint clockRateActual, Module module) public Result GetOld(out uint clockRateActual, Module module)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
lock (_sessionList) lock (_sessionList)
{ {
Session session = GetSessionByModule(module); clockRateActual = GetEffectiveClockRate(module);
clockRateActual = session == null ? 0 : session.ClockRateMin;
} }
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get module={module} actual={clockRateActual}");
return Result.Success; return Result.Success;
} }
@@ -70,51 +76,76 @@ namespace Ryujinx.Horizon.MmNv.Ipc
{ {
bool isAutoClearEvent = autoClearEvent != 0; bool isAutoClearEvent = autoClearEvent != 0;
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
requestId = Register(module, fgmPriority, isAutoClearEvent); requestId = Register(module, fgmPriority, isAutoClearEvent);
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
return Result.Success; return Result.Success;
} }
[CmifCommand(5)] [CmifCommand(5)]
public Result Finalize(uint requestId) public Result Finalize(uint requestId)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
lock (_sessionList) lock (_sessionList)
{ {
_sessionList.Remove(GetSessionById(requestId)); Session session = GetSessionById(requestId);
if (session != null)
{
_sessionList.Remove(session);
}
} }
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized requestId={requestId}");
return Result.Success; return Result.Success;
} }
[CmifCommand(6)] [CmifCommand(6)]
public Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax) public Result SetAndWait(uint requestId, uint clockRateMin, uint timeout)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId, clockRateMin, clockRateMax }); uint actualClockRate = 0;
Module? module = null;
lock (_sessionList) lock (_sessionList)
{ {
GetSessionById(requestId)?.SetAndWait(clockRateMin, clockRateMax); Session session = GetSessionById(requestId);
if (session != null)
{
module = session.Module;
ApplyClockRequest(session, clockRateMin, timeout);
actualClockRate = GetEffectiveClockRate(session.Module);
}
} }
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait requestId={requestId} module={module?.ToString() ?? "<missing>"} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
return Result.Success; return Result.Success;
} }
[CmifCommand(7)] [CmifCommand(7)]
public Result Get(out uint clockRateActual, uint requestId) public Result Get(out uint clockRateActual, uint requestId)
{ {
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId }); Module? module = null;
lock (_sessionList) lock (_sessionList)
{ {
Session session = GetSessionById(requestId); Session session = GetSessionById(requestId);
clockRateActual = session == null ? 0 : session.ClockRateMin; if (session == null)
{
clockRateActual = 0;
}
else
{
module = session.Module;
clockRateActual = GetEffectiveClockRate(session.Module);
}
} }
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get requestId={requestId} module={module?.ToString() ?? "<missing>"} actual={clockRateActual}");
return Result.Success; return Result.Success;
} }
@@ -149,12 +180,51 @@ namespace Ryujinx.Horizon.MmNv.Ipc
lock (_sessionList) lock (_sessionList)
{ {
// Nintendo ignores the fgm priority as the other services were deprecated. // Nintendo ignores the fgm priority as the other services were deprecated.
Session session = new(_uniqueId++, module, isAutoClearEvent); Session session = RegisterSession(module, isAutoClearEvent);
_sessionList.Add(session);
return session.Id; return session.Id;
} }
} }
private Session RegisterSession(Module module, bool isAutoClearEvent)
{
Session session = new(_uniqueId++, module, isAutoClearEvent);
_sessionList.Add(session);
return session;
}
private void ApplyClockRequest(Session session, uint requestedClockRate, uint timeout)
{
if (IsClockRateSentinel(requestedClockRate))
{
session.ClearRequest(timeout);
}
else
{
session.SetAndWait(requestedClockRate, timeout);
}
}
private uint GetEffectiveClockRate(Module module)
{
uint clockRate = 0;
foreach (Session session in _sessionList)
{
if (session.Module == module && session.HasActiveRequest && session.RequestedClockRate > clockRate)
{
clockRate = session.RequestedClockRate;
}
}
return clockRate;
}
private static bool IsClockRateSentinel(uint clockRate)
{
return clockRate == 0 || unchecked((int)clockRate) < 0;
}
} }
} }

View File

@@ -7,11 +7,11 @@ namespace Ryujinx.Horizon.Sdk.MmNv
{ {
Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent); Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent);
Result FinalizeOld(Module module); Result FinalizeOld(Module module);
Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax); Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout);
Result GetOld(out uint clockRateActual, Module module); Result GetOld(out uint clockRateActual, Module module);
Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent); Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent);
Result Finalize(uint requestId); Result Finalize(uint requestId);
Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax); Result SetAndWait(uint requestId, uint clockRateMin, uint timeout);
Result Get(out uint clockRateActual, uint requestId); Result Get(out uint clockRateActual, uint requestId);
} }
} }

View File

@@ -5,22 +5,32 @@ namespace Ryujinx.Horizon.Sdk.MmNv
public Module Module { get; } public Module Module { get; }
public uint Id { get; } public uint Id { get; }
public bool IsAutoClearEvent { get; } public bool IsAutoClearEvent { get; }
public uint ClockRateMin { get; private set; } public uint RequestedClockRate { get; private set; }
public int ClockRateMax { get; private set; } public uint LastTimeout { get; private set; }
public bool HasActiveRequest { get; private set; }
public Session(uint id, Module module, bool isAutoClearEvent) public Session(uint id, Module module, bool isAutoClearEvent)
{ {
Module = module; Module = module;
Id = id; Id = id;
IsAutoClearEvent = isAutoClearEvent; IsAutoClearEvent = isAutoClearEvent;
ClockRateMin = 0; RequestedClockRate = 0;
ClockRateMax = -1; LastTimeout = uint.MaxValue;
HasActiveRequest = false;
} }
public void SetAndWait(uint clockRateMin, int clockRateMax) public void SetAndWait(uint clockRate, uint timeout)
{ {
ClockRateMin = clockRateMin; RequestedClockRate = clockRate;
ClockRateMax = clockRateMax; LastTimeout = timeout;
HasActiveRequest = true;
}
public void ClearRequest(uint timeout)
{
RequestedClockRate = 0;
LastTimeout = timeout;
HasActiveRequest = false;
} }
} }
} }