[VideoOut] added output mode events and profiling fixes

This commit is contained in:
ParantezTech 2026-09-20 06:35:19 +03:00
parent 63147d0cbd
commit 172f4e5764
4 changed files with 181 additions and 6 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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)]