diff --git a/src/SharpEmu.Libs/Rtc/RtcExports.cs b/src/SharpEmu.Libs/Rtc/RtcExports.cs index f02017f..5794ae4 100644 --- a/src/SharpEmu.Libs/Rtc/RtcExports.cs +++ b/src/SharpEmu.Libs/Rtc/RtcExports.cs @@ -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 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, diff --git a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs index 93e704b..d602e79 100644 --- a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs +++ b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs @@ -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 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", diff --git a/src/SharpEmu.Libs/UserService/UserServiceExports.cs b/src/SharpEmu.Libs/UserService/UserServiceExports.cs index 1134995..44285fa 100644 --- a/src/SharpEmu.Libs/UserService/UserServiceExports.cs +++ b/src/SharpEmu.Libs/UserService/UserServiceExports.cs @@ -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 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 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 bytes = stackalloc byte[sizeof(int)]; diff --git a/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs b/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs new file mode 100644 index 0000000..31e78d9 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs @@ -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 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 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(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(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.Empty + : reconstructed.AsSpan((y - 1) * stride, stride); + if (!TryUnfilter( + scanlines[y * scanlineLength], + sourceLine, + previousLine, + targetLine, + sourceBytesPerPixel)) + { + return false; + } + } + + pixels = GC.AllocateUninitializedArray(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 source, + ReadOnlySpan previous, + Span 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; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index f9d963e..82fc79d 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -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 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 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 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 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]; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 90d5e45..e2e8aae 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -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(