[VideoOut] added output mode events and profiling fixes
This commit is contained in:
parent
63147d0cbd
commit
172f4e5764
|
|
@ -367,8 +367,9 @@ internal static class RenderPhaseProfile
|
|||
}
|
||||
|
||||
// Cache calls on other threads must not enter the render-thread counters.
|
||||
// Check the switch first to avoid thread-local storage allocation when profiling is disabled.
|
||||
internal static Scope MeasureDetail(Phase phase) =>
|
||||
_scopeDepth > 0 ? Measure(phase) : default;
|
||||
Enabled && _scopeDepth > 0 ? Measure(phase) : default;
|
||||
|
||||
internal static bool DetailMeasurementsEnabled => Enabled && _scopeDepth > 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public static partial class VideoOutExports
|
|||
// sceVideoOutGetEventId (mapped below), so the exact value is internal; only
|
||||
// its distinctness from the flip ident matters for GetEventId/GetEventData.
|
||||
private const ulong SceVideoOutInternalEventVblank = 0x40;
|
||||
private const ulong SceVideoOutInternalEventOutputMode = 0x8;
|
||||
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
|
|
@ -239,6 +240,7 @@ public static partial class VideoOutExports
|
|||
public uint OutputWidth { get; set; } = 1920;
|
||||
public uint OutputHeight { get; set; } = 1080;
|
||||
public uint RefreshRate { get; set; } = 60;
|
||||
public ulong OutputMode { get; set; } = SceVideoOutOutputModeDefault;
|
||||
public float Gamma { get; set; } = 1.0f;
|
||||
public Dictionary<long, long> LatencyStartPoints { get; } = new();
|
||||
public Queue<(long FlipArg, long Timestamp)> LatencyStartPointOrder { get; } = new();
|
||||
|
|
@ -249,6 +251,7 @@ public static partial class VideoOutExports
|
|||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||
public List<FlipEventRegistration> VblankEvents { get; } = new();
|
||||
public List<FlipEventRegistration> OutputModeEvents { get; } = new();
|
||||
public long OpenTimestamp;
|
||||
public long LastCpuFlipTimestamp = -1;
|
||||
public int WindowTop;
|
||||
|
|
@ -435,10 +438,31 @@ public static partial class VideoOutExports
|
|||
if (supported < 0) return supported;
|
||||
if (supported == 0) return OrbisVideoOutErrorUnavailableOutputMode;
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
// The only advertised configuration is the current default output.
|
||||
return TryGetPort(handle, out _)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisVideoOutErrorInvalidHandle;
|
||||
var mode = ctx[CpuRegister.Rsi];
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
FlipEventRegistration[] registrations;
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.OutputMode = mode;
|
||||
registrations = port.OutputModeEvents.ToArray();
|
||||
}
|
||||
|
||||
var eventHint = (mode & 0x0000_FFFF_FFFF_FFFFUL) << 16;
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
registration.Equeue,
|
||||
SceVideoOutInternalEventOutputMode,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
registration.UserData);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -884,6 +908,52 @@ public static partial class VideoOutExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "kmSe30JTs+E",
|
||||
ExportName = "sceVideoOutAddOutputModeEvent",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutAddOutputModeEvent(CpuContext ctx)
|
||||
{
|
||||
var equeue = ctx[CpuRegister.Rdi];
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var userData = ctx[CpuRegister.Rdx];
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEventQueue;
|
||||
}
|
||||
|
||||
ulong outputMode;
|
||||
lock (_stateGate)
|
||||
{
|
||||
var existingIndex = port.OutputModeEvents.FindIndex(registration => registration.Equeue == equeue);
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
port.OutputModeEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
|
||||
}
|
||||
else
|
||||
{
|
||||
port.OutputModeEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||
}
|
||||
outputMode = port.OutputMode;
|
||||
}
|
||||
|
||||
var eventHint = (outputMode & 0x0000_FFFF_FFFF_FFFFUL) << 16;
|
||||
return KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
equeue,
|
||||
SceVideoOutInternalEventOutputMode,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
userData)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisVideoOutErrorInvalidEventQueue;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "oNOQn3knW6s",
|
||||
ExportName = "sceVideoOutDeleteVblankEvent",
|
||||
|
|
@ -1025,6 +1095,11 @@ public static partial class VideoOutExports
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (ident == SceVideoOutInternalEventOutputMode)
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
||||
|
|
@ -1050,7 +1125,9 @@ public static partial class VideoOutExports
|
|||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut ||
|
||||
(ident != SceVideoOutInternalEventFlip && ident != SceVideoOutInternalEventVblank))
|
||||
(ident != SceVideoOutInternalEventFlip &&
|
||||
ident != SceVideoOutInternalEventVblank &&
|
||||
ident != SceVideoOutInternalEventOutputMode))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class RenderPhaseProfileTests
|
||||
{
|
||||
[Fact]
|
||||
public void DisabledDetailMeasurement_DoesNotAllocateOnANewThread()
|
||||
{
|
||||
if (RenderPhaseProfile.Enabled) return;
|
||||
|
||||
long allocated = -1;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking))
|
||||
{
|
||||
}
|
||||
allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
}) { IsBackground = true };
|
||||
thread.Start();
|
||||
Assert.True(thread.Join(TimeSpan.FromSeconds(30)));
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailMeasurement_PreservesTheOuterScope()
|
||||
{
|
||||
Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled);
|
||||
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Draw))
|
||||
{
|
||||
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking))
|
||||
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
|
||||
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
|
||||
}
|
||||
Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled);
|
||||
}
|
||||
}
|
||||
|
|
@ -234,6 +234,60 @@ public sealed class VideoOutFlipRequestTests : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputModeEventIsImmediatelyAvailableAndTracksConfiguration()
|
||||
{
|
||||
_context[CpuRegister.Rdi] = StatusAddress;
|
||||
_context[CpuRegister.Rsi] = 0;
|
||||
Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(_context));
|
||||
var bytes = new byte[8];
|
||||
Assert.True(_memory.TryRead(StatusAddress, bytes));
|
||||
var queue = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
try
|
||||
{
|
||||
const ulong userData = 0x1234_5678_9ABC_DEF0;
|
||||
_context[CpuRegister.Rdi] = queue;
|
||||
_context[CpuRegister.Rsi] = (ulong)_handle;
|
||||
_context[CpuRegister.Rdx] = userData;
|
||||
Assert.Equal(0, VideoOutExports.VideoOutAddOutputModeEvent(_context));
|
||||
|
||||
AssertOutputModeEvent(queue, expectedMode: 1, userData);
|
||||
|
||||
_context[CpuRegister.Rdi] = (ulong)_handle;
|
||||
_context[CpuRegister.Rsi] = 1;
|
||||
_context[CpuRegister.Rdx] = 0;
|
||||
_context[CpuRegister.Rcx] = 0;
|
||||
_context[CpuRegister.R8] = 0;
|
||||
Assert.Equal(0, VideoOutExports.VideoOutConfigureOutput(_context));
|
||||
|
||||
AssertOutputModeEvent(queue, expectedMode: 1, userData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context[CpuRegister.Rdi] = queue;
|
||||
Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(_context));
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertOutputModeEvent(ulong queue, ulong expectedMode, ulong userData)
|
||||
{
|
||||
var timeoutAddress = MemoryBase + 0x400;
|
||||
Assert.True(_memory.TryWrite(timeoutAddress, BitConverter.GetBytes(1_000_000u)));
|
||||
_context[CpuRegister.Rdi] = queue;
|
||||
_context[CpuRegister.Rsi] = MemoryBase + 0x200;
|
||||
_context[CpuRegister.Rdx] = 1;
|
||||
_context[CpuRegister.Rcx] = MemoryBase + 0x300;
|
||||
_context[CpuRegister.R8] = timeoutAddress;
|
||||
Assert.Equal(0, KernelEventQueueCompatExports.KernelWaitEqueue(_context));
|
||||
|
||||
var eventBytes = new byte[0x20];
|
||||
Assert.True(_memory.TryRead(MemoryBase + 0x200, eventBytes));
|
||||
Assert.Equal(8UL, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes));
|
||||
Assert.Equal(-13, BinaryPrimitives.ReadInt16LittleEndian(eventBytes.AsSpan(0x08)));
|
||||
Assert.Equal(expectedMode, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x10)) >> 16);
|
||||
Assert.Equal(userData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x18)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue