mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-14 12:56:14 +08:00
[libs] Update system and video exports
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user