[libs] Update system and video exports

This commit is contained in:
ParantezTech
2026-06-28 23:46:56 +03:00
parent a04e70c0b3
commit 4ae778ef39
6 changed files with 601 additions and 9 deletions
+85 -2
View File
@@ -8,6 +8,8 @@ namespace SharpEmu.Libs.Rtc;
public static class RtcExports
{
private const long DateTimeTicksPerMicrosecond = 10;
[SysAbiExport(
Nid = "ZPD1YOKI+Kw",
ExportName = "sceRtcGetCurrentClockLocalTime",
@@ -42,6 +44,29 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "18B2NS1y9UU",
ExportName = "sceRtcGetCurrentTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentTick(CpuContext ctx)
{
var tickAddress = ctx[CpuRegister.Rdi];
if (tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8w-H19ip48I",
ExportName = "sceRtcGetTick",
@@ -72,14 +97,57 @@ public static class RtcExports
rtcDateTime.Minute,
rtcDateTime.Second,
DateTimeKind.Utc);
tickValue = checked((ulong)((baseDateTime.Ticks / 10) + rtcDateTime.Microsecond));
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ueega6v3GUw",
ExportName = "sceRtcSetTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetTick(CpuContext ctx)
{
var dateTimeAddress = ctx[CpuRegister.Rdi];
var tickAddress = ctx[CpuRegister.Rsi];
if (dateTimeAddress == 0 || tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
DateTime dateTime;
try
{
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
}
catch (ArgumentOutOfRangeException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -108,6 +176,21 @@ public static class RtcExports
return true;
}
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
{
Span<byte> rtcDateTime = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
BinaryPrimitives.WriteUInt32LittleEndian(
rtcDateTime[12..16],
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
return ctx.Memory.TryWrite(address, rtcDateTime);
}
private readonly record struct RtcDateTime(
ushort Year,
ushort Month,
@@ -12,6 +12,34 @@ public static class SystemServiceExports
private const int SystemServiceStatusSize = 0x0C;
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
[SysAbiExport(
Nid = "fZo48un7LK4",
ExportName = "sceSystemServiceParamGetInt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceParamGetInt(CpuContext ctx)
{
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
}
var value = parameterId switch
{
1 or 2 or 3 or 1000 => 1,
4 => 180,
_ => 0,
};
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
return ctx.Memory.TryWrite(valueAddress, valueBytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "rPo6tV8D9bM",
ExportName = "sceSystemServiceGetStatus",
@@ -3,14 +3,21 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService;
public static class UserServiceExports
{
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
private const int PrimaryUserId = 1;
private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
[SysAbiExport(
Nid = "j3YMu1MVNNo",
@@ -64,6 +71,89 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "yH17Q6NWtVg",
ExportName = "sceUserServiceGetEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetEvent(CpuContext ctx)
{
var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
{
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
}
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
return ctx.Memory.TryWrite(eventAddress, payload)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "1xxcMiGu2fo",
ExportName = "sceUserServiceGetUserName",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetUserName(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (nameAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
if (capacity <= (ulong)nameBytes.Length)
{
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
}
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "D-CzAxQL0XI",
ExportName = "sceUserServiceGetPlatformPrivacySetting",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetPlatformPrivacySetting(CpuContext ctx)
{
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
var valueAddress = ctx[CpuRegister.Rsi];
if (parameterId != 1000)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return TryWriteInt32(ctx, valueAddress, 0)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
@@ -0,0 +1,213 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.IO.Compression;
namespace SharpEmu.Libs.VideoOut;
internal static class PngSplashLoader
{
private static ReadOnlySpan<byte> PngSignature =>
[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
];
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
{
pixels = [];
width = 0;
height = 0;
try
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return false;
}
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
if (!File.Exists(path))
{
return false;
}
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
}
catch
{
pixels = [];
width = 0;
height = 0;
return false;
}
}
private static bool TryDecode(
ReadOnlySpan<byte> png,
out byte[] pixels,
out uint width,
out uint height)
{
pixels = [];
width = 0;
height = 0;
if (png.Length < 33 || !png[..8].SequenceEqual(PngSignature))
{
return false;
}
byte bitDepth = 0;
byte colorType = 0;
byte interlace = 0;
using var compressed = new MemoryStream();
var offset = 8;
while (offset <= png.Length - 12)
{
var chunkLength = BinaryPrimitives.ReadUInt32BigEndian(png.Slice(offset, 4));
if (chunkLength > int.MaxValue || offset > png.Length - 12 - (int)chunkLength)
{
return false;
}
var chunkType = png.Slice(offset + 4, 4);
var chunkData = png.Slice(offset + 8, (int)chunkLength);
if (chunkType.SequenceEqual("IHDR"u8))
{
if (chunkData.Length != 13)
{
return false;
}
width = BinaryPrimitives.ReadUInt32BigEndian(chunkData[..4]);
height = BinaryPrimitives.ReadUInt32BigEndian(chunkData.Slice(4, 4));
bitDepth = chunkData[8];
colorType = chunkData[9];
interlace = chunkData[12];
}
else if (chunkType.SequenceEqual("IDAT"u8))
{
compressed.Write(chunkData);
}
else if (chunkType.SequenceEqual("IEND"u8))
{
break;
}
offset += checked((int)chunkLength + 12);
}
var sourceBytesPerPixel = colorType switch
{
2 => 3,
6 => 4,
_ => 0,
};
if (width == 0 ||
height == 0 ||
width > 16384 ||
height > 16384 ||
bitDepth != 8 ||
interlace != 0 ||
sourceBytesPerPixel == 0 ||
compressed.Length == 0)
{
return false;
}
var stride = checked((int)width * sourceBytesPerPixel);
var scanlineLength = checked(stride + 1);
var decompressedLength = checked(scanlineLength * (int)height);
var scanlines = GC.AllocateUninitializedArray<byte>(decompressedLength);
compressed.Position = 0;
using (var zlib = new ZLibStream(compressed, CompressionMode.Decompress))
{
zlib.ReadExactly(scanlines);
if (zlib.ReadByte() != -1)
{
return false;
}
}
var reconstructed = GC.AllocateUninitializedArray<byte>(checked(stride * (int)height));
for (var y = 0; y < (int)height; y++)
{
var sourceLine = scanlines.AsSpan(y * scanlineLength + 1, stride);
var targetLine = reconstructed.AsSpan(y * stride, stride);
var previousLine = y == 0
? ReadOnlySpan<byte>.Empty
: reconstructed.AsSpan((y - 1) * stride, stride);
if (!TryUnfilter(
scanlines[y * scanlineLength],
sourceLine,
previousLine,
targetLine,
sourceBytesPerPixel))
{
return false;
}
}
pixels = GC.AllocateUninitializedArray<byte>(checked((int)width * (int)height * 4));
for (int sourceOffset = 0, targetOffset = 0;
sourceOffset < reconstructed.Length;
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
{
pixels[targetOffset] = reconstructed[sourceOffset + 2];
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
pixels[targetOffset + 2] = reconstructed[sourceOffset];
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
? reconstructed[sourceOffset + 3]
: (byte)0xFF;
}
return true;
}
private static bool TryUnfilter(
byte filter,
ReadOnlySpan<byte> source,
ReadOnlySpan<byte> previous,
Span<byte> target,
int bytesPerPixel)
{
for (var x = 0; x < source.Length; x++)
{
var left = x >= bytesPerPixel ? target[x - bytesPerPixel] : (byte)0;
var above = previous.IsEmpty ? (byte)0 : previous[x];
var upperLeft = !previous.IsEmpty && x >= bytesPerPixel
? previous[x - bytesPerPixel]
: (byte)0;
target[x] = filter switch
{
0 => source[x],
1 => unchecked((byte)(source[x] + left)),
2 => unchecked((byte)(source[x] + above)),
3 => unchecked((byte)(source[x] + ((left + above) >> 1))),
4 => unchecked((byte)(source[x] + Paeth(left, above, upperLeft))),
_ => source[x],
};
if (filter > 4)
{
return false;
}
}
return true;
}
private static byte Paeth(byte left, byte above, byte upperLeft)
{
var estimate = left + above - upperLeft;
var leftDistance = Math.Abs(estimate - left);
var aboveDistance = Math.Abs(estimate - above);
var upperLeftDistance = Math.Abs(estimate - upperLeft);
return leftDistance <= aboveDistance && leftDistance <= upperLeftDistance
? left
: aboveDistance <= upperLeftDistance
? above
: upperLeft;
}
}
+100 -1
View File
@@ -29,8 +29,11 @@ public static class VideoOutExports
private const int VideoOutBufferAttributeSize = 0x28;
private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20;
private const int VideoOutOutputStatusSize = 0x30;
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
private const ulong SceVideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
private const ulong SceVideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
@@ -82,6 +85,10 @@ public static class VideoOutExports
public ulong VblankCount { get; set; }
public ulong FlipCount { get; set; }
public int CurrentBuffer { get; set; } = -1;
public uint OutputWidth { get; set; } = 1920;
public uint OutputHeight { get; set; } = 1080;
public uint RefreshRate { get; set; } = 60;
public float Gamma { get; set; } = 1.0f;
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> FlipEvents { get; } = new();
@@ -196,6 +203,93 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "utPrVdxio-8",
ExportName = "sceVideoOutGetOutputStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetOutputStatus(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
status.Clear();
var resolutionClass = port.OutputWidth >= 3840 || port.OutputHeight >= 2160 ? 2 : 1;
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], 1);
BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], port.RefreshRate);
return ctx.Memory.TryWrite(statusAddress, status)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "DYhhWbJSeRg",
ExportName = "sceVideoOutColorSettingsSetGamma_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutColorSettingsSetGamma(CpuContext ctx)
{
var settingsAddress = ctx[CpuRegister.Rdi];
if (settingsAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
ctx.GetXmmRegister(0, out var xmm0Low, out _);
var gamma = BitConverter.Int32BitsToSingle(unchecked((int)xmm0Low));
if (!float.IsFinite(gamma) || gamma is < 0.1f or > 2.0f)
{
return OrbisVideoOutErrorInvalidValue;
}
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
BinaryPrimitives.WriteInt32LittleEndian(gammaBytes, BitConverter.SingleToInt32Bits(gamma));
return ctx.Memory.TryWrite(settingsAddress, gammaBytes)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "pv9CI5VC+R0",
ExportName = "sceVideoOutAdjustColor_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAdjustColor(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var settingsAddress = ctx[CpuRegister.Rsi];
if (settingsAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
if (!ctx.Memory.TryRead(settingsAddress, gammaBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
port.Gamma = BitConverter.Int32BitsToSingle(
BinaryPrimitives.ReadInt32LittleEndian(gammaBytes));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "j6RaAUlaLv0",
ExportName = "sceVideoOutWaitVblank",
@@ -715,6 +809,8 @@ public static class VideoOutExports
Index = groupIndex,
Attribute = attribute,
};
port.OutputWidth = attribute.Width;
port.OutputHeight = attribute.Height;
for (var i = 0; i < addresses.Length; i++)
{
@@ -726,6 +822,7 @@ public static class VideoOutExports
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
return groupIndex;
}
}
@@ -939,6 +1036,8 @@ public static class VideoOutExports
private static uint GetBytesPerPixel(ulong pixelFormat) =>
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatB8G8R8A8Unorm or
SceVideoOutPixelFormatR8G8B8A8Unorm or
SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
@@ -973,7 +1072,7 @@ public static class VideoOutExports
var dst = 0;
for (var src = 0; src + 3 < source.Length; src += 4)
{
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
if (pixelFormat is SceVideoOutPixelFormatA8B8G8R8Srgb or SceVideoOutPixelFormatR8G8B8A8Unorm)
{
destination[dst++] = source[src + 0];
destination[dst++] = source[src + 1];
@@ -23,8 +23,62 @@ internal static unsafe class VulkanVideoPresenter
private static readonly object _gate = new();
private static Thread? _thread;
private static Presentation? _latestPresentation;
private static uint _windowWidth;
private static uint _windowHeight;
private static bool _closed;
public static void EnsureStarted(uint width, uint height)
{
if (width == 0 || height == 0)
{
return;
}
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
}
var hasSplash = PngSplashLoader.TryLoad(
out var splashPixels,
out var splashWidth,
out var splashHeight);
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_latestPresentation ??= hasSplash
? new Presentation(
splashPixels,
splashWidth,
splashHeight,
1,
GuestDrawKind.None,
IsSplash: true)
: new Presentation(
null,
width,
height,
0,
GuestDrawKind.None,
IsSplash: false);
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void Submit(byte[] bgraFrame, uint width, uint height)
{
if (bgraFrame.Length != checked((int)(width * height * 4)))
@@ -40,12 +94,20 @@ internal static unsafe class VulkanVideoPresenter
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(bgraFrame, width, height, sequence, GuestDrawKind.None);
_latestPresentation = new Presentation(
bgraFrame,
width,
height,
sequence,
GuestDrawKind.None,
IsSplash: false);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
@@ -74,12 +136,20 @@ internal static unsafe class VulkanVideoPresenter
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(null, width, height, sequence, drawKind);
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
drawKind,
IsSplash: false);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
@@ -95,8 +165,8 @@ internal static unsafe class VulkanVideoPresenter
uint height;
lock (_gate)
{
width = _latestPresentation?.Width ?? 1280;
height = _latestPresentation?.Height ?? 720;
width = _windowWidth == 0 ? _latestPresentation?.Width ?? 1280 : _windowWidth;
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
}
try
@@ -138,7 +208,8 @@ internal static unsafe class VulkanVideoPresenter
uint Width,
uint Height,
long Sequence,
GuestDrawKind DrawKind);
GuestDrawKind DrawKind,
bool IsSplash);
private sealed class Presenter : IDisposable
{
@@ -179,6 +250,7 @@ internal static unsafe class VulkanVideoPresenter
private bool _vulkanReady;
private bool _firstFramePresented;
private bool _firstGuestDrawPresented;
private bool _splashPresented;
public Presenter(uint width, uint height)
{
@@ -813,7 +885,14 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
_imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence;
if (!_firstFramePresented)
if (presentation.IsSplash && !_splashPresented)
{
_splashPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented splash: " +
$"{presentation.Width}x{presentation.Height}");
}
else if (!presentation.IsSplash && !_firstFramePresented)
{
_firstFramePresented = true;
Console.Error.WriteLine(