From 1ea13969796153ece8795eef5092e4f86913280d Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Sun, 7 Jun 2026 15:43:26 +0300 Subject: [PATCH] [kernel/videoOut] extend memory managements and videoOut (this is not a swapchain) --- src/SharpEmu.Libs/Agc/AgcExports.cs | 1121 +++++++++++++++++ .../Kernel/KernelEventQueueCompatExports.cs | 106 +- .../Kernel/KernelMemoryCompatExports.cs | 115 +- .../Kernel/KernelSemaphoreCompatExports.cs | 318 +++++ src/SharpEmu.Libs/Pad/PadExports.cs | 158 +++ .../SystemService/SystemServiceExports.cs | 42 + .../UserService/UserServiceExports.cs | 79 ++ src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 722 ++++++++++- 8 files changed, 2632 insertions(+), 29 deletions(-) create mode 100644 src/SharpEmu.Libs/Agc/AgcExports.cs create mode 100644 src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs create mode 100644 src/SharpEmu.Libs/Pad/PadExports.cs create mode 100644 src/SharpEmu.Libs/SystemService/SystemServiceExports.cs create mode 100644 src/SharpEmu.Libs/UserService/UserServiceExports.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs new file mode 100644 index 0000000..5348d41 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -0,0 +1,1121 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.VideoOut; +using System.Buffers.Binary; +using System.Threading; + +namespace SharpEmu.Libs.Agc; + +public static class AgcExports +{ + private const uint ShaderFileHeader = 0x34333231; + private const uint ShaderVersion = 0x18; + private const uint ItNop = 0x10; + private const uint ItIndexBufferSize = 0x13; + private const uint ItIndexBase = 0x26; + private const uint ItIndexType = 0x2A; + private const uint ItDrawIndexOffset2 = 0x35; + private const uint ItSetShReg = 0x76; + private const uint RZero = 0x00; + private const uint RDrawIndexAuto = 0x04; + private const uint RDrawReset = 0x05; + private const uint RWaitFlipDone = 0x06; + private const uint RShRegsIndirect = 0x11; + private const uint RCxRegsIndirect = 0x12; + private const uint RUcRegsIndirect = 0x13; + private const uint RFlip = 0x17; + private const uint SpiShaderPgmLoPs = 0x8; + private const uint SpiShaderPgmHiPs = 0x9; + private const uint SpiShaderPgmLoEs = 0xC8; + private const uint SpiShaderPgmHiEs = 0xC9; + private const uint SpiPsInputCntl0 = 0x191; + private const uint VgtPrimitiveType = 0x242; + private const uint PsTextureUserDataRegister = 0xC; + private const uint Gen5TextureFormatR8G8B8A8Unorm = 56; + private const uint Gen5TextureType2D = 9; + private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; + private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; + + private const ulong ShaderUserDataOffset = 0x08; + private const ulong ShaderCodeOffset = 0x10; + private const ulong ShaderCxRegistersOffset = 0x18; + private const ulong ShaderShRegistersOffset = 0x20; + private const ulong ShaderSpecialsOffset = 0x28; + private const ulong ShaderInputSemanticsOffset = 0x30; + private const ulong ShaderOutputSemanticsOffset = 0x38; + private const ulong ShaderNumInputSemanticsOffset = 0x50; + private const ulong ShaderNumOutputSemanticsOffset = 0x56; + private const ulong ShaderTypeOffset = 0x5A; + private const ulong ShaderNumShRegistersOffset = 0x5C; + private const ulong CommandBufferCursorUpOffset = 0x10; + private const ulong CommandBufferCursorDownOffset = 0x18; + private const ulong CommandBufferCallbackOffset = 0x20; + private const ulong CommandBufferReservedDwOffset = 0x30; + private const ulong ShaderSpecialGeCntlOffset = 0x00; + private const ulong ShaderSpecialVgtShaderStagesEnOffset = 0x08; + private const ulong ShaderSpecialVgtGsOutPrimTypeOffset = 0x20; + private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28; + private const uint CbSetShRegisterRangeMarker = 0x6875000D; + private static readonly object _softwarePresenterGate = new(); + private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new(); + private static int _submitTraceCaptured; + + private readonly record struct TextureDescriptor( + ulong Address, + uint Width, + uint Height, + uint Format, + uint TileMode, + uint Type); + + [SysAbiExport( + Nid = "f3dg2CSgRKY", + ExportName = "sceAgcCreateShader", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int CreateShader(CpuContext ctx) + { + var destinationAddress = ctx[CpuRegister.Rdi]; + var headerAddress = ctx[CpuRegister.Rsi]; + var codeAddress = ctx[CpuRegister.Rdx]; + if (destinationAddress == 0 || headerAddress == 0 || codeAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryReadUInt32(ctx, headerAddress, out var fileHeader) || + !TryReadUInt32(ctx, headerAddress + sizeof(uint), out var version)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (fileHeader != ShaderFileHeader || version != ShaderVersion) + { + TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) || + !RelocatePointerField(ctx, headerAddress + ShaderShRegistersOffset) || + !RelocatePointerField(ctx, headerAddress + ShaderUserDataOffset) || + !RelocatePointerField(ctx, headerAddress + ShaderSpecialsOffset) || + !RelocatePointerField(ctx, headerAddress + ShaderInputSemanticsOffset) || + !RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) || + !ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (!TryReadUInt64(ctx, headerAddress + ShaderUserDataOffset, out var userDataAddress)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (userDataAddress != 0 && + (!RelocatePointerField(ctx, userDataAddress) || + !RelocatePointerField(ctx, userDataAddress + 0x08) || + !RelocatePointerField(ctx, userDataAddress + 0x10) || + !RelocatePointerField(ctx, userDataAddress + 0x18) || + !RelocatePointerField(ctx, userDataAddress + 0x20))) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!ctx.TryWriteUInt64(destinationAddress, headerAddress)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceCreateShader(destinationAddress, headerAddress, codeAddress, "ok"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "vcmNN+AAXnY", + ExportName = "sceAgcSetCxRegIndirectPatchSetAddress", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetCxRegIndirectPatchSetAddress(CpuContext ctx) => + SetIndirectPatchAddress(ctx, "cx"); + + [SysAbiExport( + Nid = "Qrj4c+61z4A", + ExportName = "sceAgcSetShRegIndirectPatchSetAddress", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetShRegIndirectPatchSetAddress(CpuContext ctx) => + SetIndirectPatchAddress(ctx, "sh"); + + [SysAbiExport( + Nid = "6lNcCp+fxi4", + ExportName = "sceAgcSetUcRegIndirectPatchSetAddress", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetUcRegIndirectPatchSetAddress(CpuContext ctx) => + SetIndirectPatchAddress(ctx, "uc"); + + [SysAbiExport( + Nid = "d-6uF9sZDIU", + ExportName = "sceAgcSetCxRegIndirectPatchAddRegisters", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetCxRegIndirectPatchAddRegisters(CpuContext ctx) => + AddIndirectPatchRegisters(ctx, "cx"); + + [SysAbiExport( + Nid = "z2duB-hHQSM", + ExportName = "sceAgcSetShRegIndirectPatchAddRegisters", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetShRegIndirectPatchAddRegisters(CpuContext ctx) => + AddIndirectPatchRegisters(ctx, "sh"); + + [SysAbiExport( + Nid = "vRoArM9zaIk", + ExportName = "sceAgcSetUcRegIndirectPatchAddRegisters", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetUcRegIndirectPatchAddRegisters(CpuContext ctx) => + AddIndirectPatchRegisters(ctx, "uc"); + + [SysAbiExport( + Nid = "D9sr1xGUriE", + ExportName = "sceAgcCreatePrimState", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int CreatePrimState(CpuContext ctx) + { + var cxRegistersAddress = ctx[CpuRegister.Rdi]; + var ucRegistersAddress = ctx[CpuRegister.Rsi]; + var hullShaderAddress = ctx[CpuRegister.Rdx]; + var geometryShaderAddress = ctx[CpuRegister.Rcx]; + var primitiveType = (uint)ctx[CpuRegister.R8]; + + if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || shaderType != 2 || + !TryReadUInt64(ctx, geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) || + specialsAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) || + !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtGsOutPrimTypeOffset, cxRegistersAddress + 8) || + !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialGeCntlOffset, ucRegistersAddress) || + !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialGeUserVgprEnOffset, ucRegistersAddress + 8) || + !TryWriteUInt32(ctx, ucRegistersAddress + 16, VgtPrimitiveType) || + !TryWriteUInt32(ctx, ucRegistersAddress + 20, primitiveType)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} prim=0x{primitiveType:X8}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "HV4j+E0MBHE", + ExportName = "sceAgcCreateInterpolantMapping", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int CreateInterpolantMapping(CpuContext ctx) + { + var registersAddress = ctx[CpuRegister.Rdi]; + var geometryShaderAddress = ctx[CpuRegister.Rsi]; + var pixelShaderAddress = ctx[CpuRegister.Rdx]; + + if (registersAddress == 0 || geometryShaderAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryReadUInt64(ctx, geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) || + !TryReadUInt32(ctx, geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + ulong inputSemanticsAddress = 0; + if (pixelShaderAddress != 0 && + (!TryReadUInt64(ctx, pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) || + !TryReadUInt32(ctx, pixelShaderAddress + ShaderNumInputSemanticsOffset, out _))) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + for (uint i = 0; i < 32; i++) + { + uint value = 0; + if (i < outputSemanticsCount && outputSemanticsAddress != 0) + { + var flat = false; + if (pixelShaderAddress != 0 && inputSemanticsAddress != 0 && + TryReadUInt32(ctx, inputSemanticsAddress + (i * sizeof(uint)), out var inputSemantic)) + { + flat = ((inputSemantic >> 22) & 0x1) != 0; + } + + value = i | (flat ? 0x400u : 0u); + } + + var destination = registersAddress + (i * 8); + if (!TryWriteUInt32(ctx, destination, SpiPsInputCntl0 + i) || + !TryWriteUInt32(ctx, destination + sizeof(uint), value)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + } + + TraceAgc($"agc.create_interpolant_mapping regs=0x{registersAddress:X16} gs=0x{geometryShaderAddress:X16} ps=0x{pixelShaderAddress:X16}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "V++UgBtQhn0", + ExportName = "sceAgcGetDataPacketPayloadAddress", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int GetDataPacketPayloadAddress(CpuContext ctx) + { + var outputAddress = ctx[CpuRegister.Rdi]; + var commandAddress = ctx[CpuRegister.Rsi]; + var type = (int)ctx[CpuRegister.Rdx]; + if (outputAddress == 0 || commandAddress == 0 || type != 1) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!ctx.TryWriteUInt64(outputAddress, commandAddress + 8)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.get_packet_payload out=0x{outputAddress:X16} cmd=0x{commandAddress:X16} payload=0x{commandAddress + 8:X16}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "n2fD4A+pb+g", + ExportName = "sceAgcCbSetShRegisterRangeDirect", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int CbSetShRegisterRangeDirect(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var offset = (uint)ctx[CpuRegister.Rsi]; + var valuesAddress = ctx[CpuRegister.Rdx]; + var valueCount = (uint)ctx[CpuRegister.Rcx]; + if (commandBufferAddress == 0 || offset == 0 || offset > 0x3FF || valueCount == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var markerAddress) || + !TryWriteUInt32(ctx, markerAddress, Pm4(2, ItNop, RZero)) || + !TryWriteUInt32(ctx, markerAddress + 4, CbSetShRegisterRangeMarker) || + !TryAllocateCommandDwords(ctx, commandBufferAddress, valueCount + 2, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(valueCount + 2, ItSetShReg, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, offset)) + { + return ReturnPointer(ctx, 0); + } + + for (uint i = 0; i < valueCount; i++) + { + var value = 0u; + if (valuesAddress != 0 && + !TryReadUInt32(ctx, valuesAddress + (i * sizeof(uint)), out value)) + { + return ReturnPointer(ctx, 0); + } + + if (!TryWriteUInt32(ctx, commandAddress + 8 + (i * sizeof(uint)), value)) + { + return ReturnPointer(ctx, 0); + } + } + + TraceAgc($"agc.cb_set_sh_range buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} offset=0x{offset:X8} count={valueCount}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "TRO721eVt4g", + ExportName = "sceAgcDcbResetQueue", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbResetQueue(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var op = (uint)ctx[CpuRegister.Rsi]; + var state = (uint)ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0 || op != 0x3FF || state != 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RDrawReset)) || + !TryWriteUInt32(ctx, commandAddress + 4, 0)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_reset_queue buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "ZvwO9euwYzc", + ExportName = "sceAgcDcbSetCxRegistersIndirect", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetCxRegistersIndirect(CpuContext ctx) => + DcbSetRegistersIndirect(ctx, RCxRegsIndirect, "cx"); + + [SysAbiExport( + Nid = "-HOOCn0JY48", + ExportName = "sceAgcDcbSetShRegistersIndirect", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetShRegistersIndirect(CpuContext ctx) => + DcbSetRegistersIndirect(ctx, RShRegsIndirect, "sh"); + + [SysAbiExport( + Nid = "hvUfkUIQcOE", + ExportName = "sceAgcDcbSetUcRegistersIndirect", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetUcRegistersIndirect(CpuContext ctx) => + DcbSetRegistersIndirect(ctx, RUcRegsIndirect, "uc"); + + [SysAbiExport( + Nid = "GIIW2J37e70", + ExportName = "sceAgcDcbSetIndexSize", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetIndexSize(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var indexSize = (uint)(ctx[CpuRegister.Rsi] & 0xFF); + var cachePolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF); + if (commandBufferAddress == 0 || cachePolicy != 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(2, ItIndexType, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, indexSize)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_set_index_size buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} size={indexSize}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "l4fM9K-Lyks", + ExportName = "sceAgcDcbSetIndexBuffer", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetIndexBuffer(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var indexBufferAddress = ctx[CpuRegister.Rsi]; + var indexCount = (uint)ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(3, ItIndexBase, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, (uint)(indexBufferAddress & 0xFFFF_FFFFUL)) || + !TryWriteUInt32(ctx, commandAddress + 8, (uint)(indexBufferAddress >> 32)) || + !TryWriteUInt32(ctx, commandAddress + 12, Pm4(2, ItIndexBufferSize, 0)) || + !TryWriteUInt32(ctx, commandAddress + 16, indexCount)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_set_index_buffer buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} addr=0x{indexBufferAddress:X16} count={indexCount}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "B+aG9DUnTKA", + ExportName = "sceAgcDcbDrawIndexOffset", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbDrawIndexOffset(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var indexOffset = (uint)ctx[CpuRegister.Rsi]; + var indexCount = (uint)ctx[CpuRegister.Rdx]; + var flags = (uint)ctx[CpuRegister.Rcx]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDrawIndexOffset2, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, indexCount) || + !TryWriteUInt32(ctx, commandAddress + 8, indexOffset) || + !TryWriteUInt32(ctx, commandAddress + 12, indexCount) || + !TryWriteUInt32(ctx, commandAddress + 16, flags & 0xE000_0001u)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_draw_index_offset buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} offset={indexOffset} count={indexCount} flags=0x{flags:X8}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "MWiElSNE8j8", + ExportName = "sceAgcDcbWaitUntilSafeForRendering", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbWaitUntilSafeForRendering(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var videoOutHandle = (uint)ctx[CpuRegister.Rsi]; + var displayBufferIndex = (uint)ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RWaitFlipDone)) || + !TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) || + !TryWriteUInt32(ctx, commandAddress + 8, displayBufferIndex) || + !TryWriteUInt32(ctx, commandAddress + 12, 0) || + !TryWriteUInt32(ctx, commandAddress + 16, 0) || + !TryWriteUInt32(ctx, commandAddress + 20, 0) || + !TryWriteUInt32(ctx, commandAddress + 24, 0)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_wait_safe buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} handle={videoOutHandle} index={displayBufferIndex}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "YUeqkyT7mEQ", + ExportName = "sceAgcDcbSetFlip", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetFlip(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var videoOutHandle = (uint)ctx[CpuRegister.Rsi]; + var displayBufferIndex = (int)ctx[CpuRegister.Rdx]; + var flipMode = (uint)ctx[CpuRegister.Rcx]; + var flipArg = unchecked((ulong)ctx[CpuRegister.R8]); + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(6, ItNop, RFlip)) || + !TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) || + !TryWriteUInt32(ctx, commandAddress + 8, unchecked((uint)displayBufferIndex)) || + !TryWriteUInt32(ctx, commandAddress + 12, flipMode) || + !TryWriteUInt32(ctx, commandAddress + 16, (uint)(flipArg & 0xFFFF_FFFFUL)) || + !TryWriteUInt32(ctx, commandAddress + 20, (uint)(flipArg >> 32))) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_set_flip buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} handle={videoOutHandle} index={displayBufferIndex} mode={flipMode} arg=0x{flipArg:X16}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "UglJIZjGssM", + ExportName = "sceAgcDriverSubmitDcb", + Target = Generation.Gen5, + LibraryName = "libSceAgcDriver")] + public static int DriverSubmitDcb(CpuContext ctx) + { + var packetAddress = ctx[CpuRegister.Rdi]; + if (packetAddress == 0 || + !TryReadUInt64(ctx, packetAddress, out var commandAddress) || + !TryReadUInt32(ctx, packetAddress + 8, out var dwordCount)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); + var tracePackets = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) && + Interlocked.CompareExchange(ref _submitTraceCaptured, 1, 0) == 0; + ParseSubmittedDcb(ctx, commandAddress, dwordCount, tracePackets); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "h9z6+0hEydk", + ExportName = "sceAgcSuspendPoint", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SuspendPoint(CpuContext ctx) + { + TraceAgc("agc.suspend_point"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static void ParseSubmittedDcb(CpuContext ctx, ulong commandAddress, uint dwordCount, bool tracePackets) + { + if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000) + { + return; + } + + TextureDescriptor? presenterTexture = null; + var sawIndexedDraw = false; + var offset = 0u; + while (offset < dwordCount) + { + var currentAddress = commandAddress + ((ulong)offset * sizeof(uint)); + if (!TryReadUInt32(ctx, currentAddress, out var header)) + { + return; + } + + var length = Pm4Length(header); + if (length == 0 || offset + length > dwordCount) + { + return; + } + + var op = (header >> 8) & 0xFFu; + var register = (header >> 2) & 0x3Fu; + if (tracePackets) + { + TraceSubmittedPacket(ctx, currentAddress, offset, header, length, op, register); + } + + if (op == ItSetShReg && + TryReadTextureDescriptor(ctx, currentAddress, length, out var texture)) + { + presenterTexture = texture; + } + + if (op == ItDrawIndexOffset2 && + length >= 5 && + TryReadUInt32(ctx, currentAddress + 4, out var indexCount) && + indexCount != 0) + { + sawIndexedDraw = true; + } + + if (op == ItNop && register == RFlip && length >= 6) + { + if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) || + !TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) || + !TryReadUInt32(ctx, currentAddress + 12, out var flipMode) || + !TryReadUInt32(ctx, currentAddress + 16, out var flipArgLo) || + !TryReadUInt32(ctx, currentAddress + 20, out var flipArgHi)) + { + return; + } + + var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo)); + var displayBufferIndex = unchecked((int)displayBufferIndexRaw); + if (sawIndexedDraw && presenterTexture is { } sourceTexture) + { + _ = TrySoftwarePresent( + ctx, + sourceTexture, + unchecked((int)videoOutHandle), + displayBufferIndex); + } + + _ = VideoOutExports.SubmitFlipFromAgc(ctx, unchecked((int)videoOutHandle), displayBufferIndex, unchecked((int)flipMode), flipArg); + } + + offset += length; + } + } + + private static bool TryReadTextureDescriptor( + CpuContext ctx, + ulong packetAddress, + uint packetLength, + out TextureDescriptor descriptor) + { + descriptor = default; + if (packetLength < 10 || + !TryReadUInt32(ctx, packetAddress + 4, out var startRegister)) + { + return false; + } + + var valueCount = packetLength - 2; + if (startRegister > PsTextureUserDataRegister || + startRegister + valueCount < PsTextureUserDataRegister + 8) + { + return false; + } + + var descriptorAddress = + packetAddress + + 8 + + ((ulong)(PsTextureUserDataRegister - startRegister) * sizeof(uint)); + Span fields = stackalloc uint[4]; + for (var i = 0; i < fields.Length; i++) + { + if (!TryReadUInt32(ctx, descriptorAddress + ((ulong)i * sizeof(uint)), out fields[i])) + { + return false; + } + } + + var address = ((((ulong)fields[1] << 32) | fields[0]) & 0xFF_FFFF_FFFFUL) << 8; + var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0xFFFu) << 2)) + 1; + var height = ((fields[2] >> 14) & 0x3FFFu) + 1; + var format = (fields[1] >> 20) & 0x1FFu; + var tileMode = (fields[3] >> 20) & 0x1Fu; + var type = (fields[3] >> 28) & 0xFu; + if (address == 0 || width == 0 || height == 0) + { + return false; + } + + descriptor = new TextureDescriptor(address, width, height, format, tileMode, type); + return true; + } + + private static bool TrySoftwarePresent( + CpuContext ctx, + TextureDescriptor source, + int videoOutHandle, + int displayBufferIndex) + { + if (source.Format != Gen5TextureFormatR8G8B8A8Unorm || + source.TileMode != 0 || + source.Type != Gen5TextureType2D || + source.Width > 8192 || + source.Height > 8192 || + !VideoOutExports.TryGetDisplayBufferInfo(videoOutHandle, displayBufferIndex, out var destination) || + destination.Address == 0 || + destination.Width == 0 || + destination.Height == 0 || + destination.Width > 8192 || + destination.Height > 8192 || + destination.TilingMode != 0 || + destination.PixelFormat is not (VideoOutPixelFormatA8R8G8B8Srgb or VideoOutPixelFormatA8B8G8R8Srgb)) + { + return false; + } + + var sourceByteCount = checked((ulong)source.Width * source.Height * 4); + if (sourceByteCount > 256UL * 1024UL * 1024UL) + { + return false; + } + + var sourceBytes = new byte[(int)sourceByteCount]; + if (!ctx.Memory.TryRead(source.Address, sourceBytes)) + { + return false; + } + + var fingerprint = ComputeFingerprint(sourceBytes); + var fingerprintKey = (source.Address, destination.Address); + lock (_softwarePresenterGate) + { + if (_softwarePresenterFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) && + previousFingerprint == fingerprint) + { + return true; + } + } + + var destinationPitch = destination.PitchInPixel == 0 + ? destination.Width + : destination.PitchInPixel; + if (destinationPitch < destination.Width) + { + return false; + } + + var destinationRow = new byte[checked((int)destinationPitch * 4)]; + var rgbaDestination = destination.PixelFormat == VideoOutPixelFormatA8B8G8R8Srgb; + for (uint y = 0; y < destination.Height; y++) + { + var sourceY = (uint)(((ulong)y * source.Height) / destination.Height); + for (uint x = 0; x < destination.Width; x++) + { + var sourceX = (uint)(((ulong)x * source.Width) / destination.Width); + var sourceOffset = checked((int)(((ulong)sourceY * source.Width + sourceX) * 4)); + var destinationOffset = checked((int)x * 4); + if (rgbaDestination) + { + destinationRow[destinationOffset + 0] = sourceBytes[sourceOffset + 0]; + destinationRow[destinationOffset + 1] = sourceBytes[sourceOffset + 1]; + destinationRow[destinationOffset + 2] = sourceBytes[sourceOffset + 2]; + } + else + { + destinationRow[destinationOffset + 0] = sourceBytes[sourceOffset + 2]; + destinationRow[destinationOffset + 1] = sourceBytes[sourceOffset + 1]; + destinationRow[destinationOffset + 2] = sourceBytes[sourceOffset + 0]; + } + + destinationRow[destinationOffset + 3] = sourceBytes[sourceOffset + 3]; + } + + var destinationAddress = destination.Address + ((ulong)y * destinationPitch * 4); + if (!ctx.Memory.TryWrite(destinationAddress, destinationRow)) + { + return false; + } + } + + lock (_softwarePresenterGate) + { + _softwarePresenterFingerprints[fingerprintKey] = fingerprint; + } + + TraceAgc( + $"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format} " + + $"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}"); + return true; + } + + private static ulong ComputeFingerprint(ReadOnlySpan bytes) + { + const ulong fnvOffsetBasis = 14695981039346656037UL; + const ulong fnvPrime = 1099511628211UL; + var fingerprint = fnvOffsetBasis; + foreach (var value in bytes) + { + fingerprint = (fingerprint ^ value) * fnvPrime; + } + + return fingerprint; + } + + private static void TraceSubmittedPacket( + CpuContext ctx, + ulong packetAddress, + uint dwordOffset, + uint header, + uint length, + uint op, + uint register) + { + TraceAgc( + $"agc.dcb.packet dw={dwordOffset} addr=0x{packetAddress:X16} header=0x{header:X8} len={length} op=0x{op:X2} reg=0x{register:X2}"); + + var payloadCount = Math.Min(length - 1, 32u); + for (uint i = 0; i < payloadCount; i++) + { + if (!TryReadUInt32(ctx, packetAddress + ((ulong)(i + 1) * sizeof(uint)), out var value)) + { + return; + } + + TraceAgc($"agc.dcb.payload dw={dwordOffset + i + 1} value=0x{value:X8}"); + } + + if (op != ItNop || + register is not (RCxRegsIndirect or RShRegsIndirect or RUcRegsIndirect) || + length < 4 || + !TryReadUInt32(ctx, packetAddress + 4, out var registerCount) || + !TryReadUInt64(ctx, packetAddress + 8, out var registersAddress)) + { + return; + } + + var registerSpace = register == RCxRegsIndirect ? "cx" : register == RShRegsIndirect ? "sh" : "uc"; + var tracedCount = Math.Min(registerCount, 256u); + TraceAgc($"agc.dcb.indirect space={registerSpace} regs=0x{registersAddress:X16} count={registerCount}"); + for (uint i = 0; i < tracedCount; i++) + { + var entryAddress = registersAddress + ((ulong)i * 8); + if (!TryReadUInt32(ctx, entryAddress, out var registerOffset) || + !TryReadUInt32(ctx, entryAddress + 4, out var value)) + { + TraceAgc($"agc.dcb.indirect_read_failed space={registerSpace} index={i} addr=0x{entryAddress:X16}"); + return; + } + + TraceAgc($"agc.dcb.reg space={registerSpace} index={i} offset=0x{registerOffset:X4} value=0x{value:X8}"); + } + + if (tracedCount != registerCount) + { + TraceAgc($"agc.dcb.indirect_truncated space={registerSpace} traced={tracedCount} total={registerCount}"); + } + } + + private static bool PatchShaderProgramRegisters(CpuContext ctx, ulong headerAddress, ulong codeAddress) + { + if (!TryReadUInt64(ctx, headerAddress + ShaderShRegistersOffset, out var shRegistersAddress) || + !TryReadByte(ctx, headerAddress + ShaderTypeOffset, out var shaderType) || + !TryReadByte(ctx, headerAddress + ShaderNumShRegistersOffset, out var registerCount)) + { + return false; + } + + if (shRegistersAddress == 0 || registerCount < 2) + { + return false; + } + + if (!TryReadUInt32(ctx, shRegistersAddress, out var loRegister) || + !TryReadUInt32(ctx, shRegistersAddress + 8, out var hiRegister)) + { + return false; + } + + var expectedLo = shaderType == 2 ? SpiShaderPgmLoEs : shaderType == 1 ? SpiShaderPgmLoPs : 0; + var expectedHi = shaderType == 2 ? SpiShaderPgmHiEs : shaderType == 1 ? SpiShaderPgmHiPs : 0; + if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi) + { + TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}"); + return false; + } + + var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); + var hiValue = (uint)((codeAddress >> 40) & 0xFFUL); + return TryWriteUInt32(ctx, shRegistersAddress + sizeof(uint), loValue) && + TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue); + } + + private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace) + { + var commandAddress = ctx[CpuRegister.Rdi]; + var registersAddress = ctx[CpuRegister.Rsi]; + if (commandAddress == 0 || registersAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryWriteUInt32(ctx, commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) || + !TryWriteUInt32(ctx, commandAddress + 12, (uint)(registersAddress >> 32))) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int AddIndirectPatchRegisters(CpuContext ctx, string registerSpace) + { + var commandAddress = ctx[CpuRegister.Rdi]; + var registerCount = (uint)ctx[CpuRegister.Rsi]; + if (commandAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryReadUInt32(ctx, commandAddress + 4, out var currentCount) || + !TryWriteUInt32(ctx, commandAddress + 4, currentCount + registerCount)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int DcbSetRegistersIndirect(CpuContext ctx, uint packetRegister, string registerSpace) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var registersAddress = ctx[CpuRegister.Rsi]; + var registerCount = (uint)ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(4, ItNop, packetRegister)) || + !TryWriteUInt32(ctx, commandAddress + 4, registerCount) || + !TryWriteUInt32(ctx, commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) || + !TryWriteUInt32(ctx, commandAddress + 12, (uint)(registersAddress >> 32))) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_set_{registerSpace}_indirect buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16} count={registerCount}"); + return ReturnPointer(ctx, commandAddress); + } + + private static bool TryAllocateCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, out ulong commandAddress) + { + commandAddress = 0; + if (sizeDwords == 0 || + !TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorUpOffset, out var cursorUp) || + !TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorDownOffset, out var cursorDown) || + !TryReadUInt64(ctx, commandBufferAddress + CommandBufferCallbackOffset, out var callback) || + !TryReadUInt32(ctx, commandBufferAddress + CommandBufferReservedDwOffset, out var reservedDwords)) + { + return false; + } + + var availableDwords = cursorDown >= cursorUp + ? Math.Min((cursorDown - cursorUp) / sizeof(uint), uint.MaxValue) + : 0; + var remainingDwords = (uint)Math.Max(availableDwords, reservedDwords) - reservedDwords; + if (sizeDwords > remainingDwords) + { + TraceAgc($"agc.cmd_alloc_full buf=0x{commandBufferAddress:X16} need={sizeDwords} remaining={remainingDwords} callback=0x{callback:X16}"); + return false; + } + + var nextCursor = cursorUp + ((ulong)sizeDwords * sizeof(uint)); + if (!ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor)) + { + return false; + } + + commandAddress = cursorUp; + return true; + } + + private static bool CopyShaderRegister(CpuContext ctx, ulong sourceAddress, ulong destinationAddress) + { + if (!TryReadUInt32(ctx, sourceAddress, out var offset) || + !TryReadUInt32(ctx, sourceAddress + sizeof(uint), out var value)) + { + return false; + } + + return TryWriteUInt32(ctx, destinationAddress, offset) && + TryWriteUInt32(ctx, destinationAddress + sizeof(uint), value); + } + + private static bool RelocatePointerField(CpuContext ctx, ulong fieldAddress) + { + if (!TryReadUInt64(ctx, fieldAddress, out var relativeAddress)) + { + return false; + } + + if (relativeAddress == 0) + { + return true; + } + + return ctx.TryWriteUInt64(fieldAddress, fieldAddress + relativeAddress); + } + + private static int ReturnPointer(CpuContext ctx, ulong pointer) + { + ctx[CpuRegister.Rax] = pointer; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int SetReturn(CpuContext ctx, OrbisGen2Result result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(int)result); + return (int)result; + } + + private static uint Pm4(uint lengthDwords, uint op, uint register) => + 0xC0000000u | + ((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) | + ((op & 0xFFu) << 8) | + ((register & 0x3Fu) << 2); + + private static uint Pm4Length(uint header) => + ((header >> 16) & 0x3FFFu) + 2u; + + private static bool TryReadByte(CpuContext ctx, ulong address, out byte value) + { + Span buffer = stackalloc byte[1]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = buffer[0]; + return true; + } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } + + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt64LittleEndian(buffer); + return true; + } + + private static void TraceAgc(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message}"); + } + + private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}"); + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 06b4eb5..eff601f 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -9,10 +9,21 @@ namespace SharpEmu.Libs.Kernel; public static class KernelEventQueueCompatExports { + private const int KernelEventSize = 0x20; + private static readonly object _eventQueueGate = new(); private static readonly HashSet _eventQueues = new(); + private static readonly Dictionary> _pendingEvents = new(); private static long _nextEventQueueHandle = 1; + public readonly record struct KernelQueuedEvent( + ulong Ident, + short Filter, + ushort Flags, + uint Fflags, + ulong Data, + ulong UserData); + [SysAbiExport( Nid = "D0OdFMjp46I", ExportName = "sceKernelCreateEqueue", @@ -30,6 +41,7 @@ public static class KernelEventQueueCompatExports lock (_eventQueueGate) { _eventQueues.Add(handle); + _pendingEvents[handle] = new Queue(); } if (!ctx.TryWriteUInt64(outAddress, handle)) @@ -52,6 +64,7 @@ public static class KernelEventQueueCompatExports lock (_eventQueueGate) { _eventQueues.Remove(handle); + _pendingEvents.Remove(handle); } TraceEventQueue(ctx, "delete", handle); @@ -165,23 +178,108 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelWaitEqueue(CpuContext ctx) { + var handle = ctx[CpuRegister.Rdi]; + var eventsAddress = ctx[CpuRegister.Rsi]; + var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue); var outCountAddress = ctx[CpuRegister.Rcx]; var timeoutAddress = ctx[CpuRegister.R8]; - if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0)) + + var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); + if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue")) + if (deliveredCount > 0) { - TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]); + TraceEventQueue(ctx, "wait-deliver", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]); + if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue")) + { + TraceEventQueue(ctx, "wait-block", handle); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + TraceEventQueue(ctx, "wait", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + public static bool IsValidEqueue(ulong handle) + { + lock (_eventQueueGate) + { + return _eventQueues.Contains(handle); + } + } + + public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent) + { + lock (_eventQueueGate) + { + if (!_eventQueues.Contains(handle)) + { + return false; + } + + if (!_pendingEvents.TryGetValue(handle, out var queue)) + { + queue = new Queue(); + _pendingEvents[handle] = queue; + } + + queue.Enqueue(queuedEvent); + return true; + } + } + + private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity) + { + if (eventsAddress == 0 || eventCapacity <= 0) + { + return 0; + } + + KernelQueuedEvent[] events; + lock (_eventQueueGate) + { + if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0) + { + return 0; + } + + var count = Math.Min(eventCapacity, queue.Count); + events = new KernelQueuedEvent[count]; + for (var i = 0; i < count; i++) + { + events[i] = queue.Dequeue(); + } + } + + for (var i = 0; i < events.Length; i++) + { + if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i])) + { + return i; + } + } + + return events.Length; + } + + private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent) + { + Span eventBytes = stackalloc byte[KernelEventSize]; + BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident); + BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter); + BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags); + BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags); + BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data); + BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData); + return ctx.Memory.TryWrite(address, eventBytes); + } + private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle) { if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index b931647..92db412 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1444,6 +1444,51 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "Oy6IpwgtYOk", + ExportName = "lseek", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixLseek(CpuContext ctx) + { + var result = KernelLseekCore( + unchecked((int)ctx[CpuRegister.Rdi]), + unchecked((long)ctx[CpuRegister.Rsi]), + unchecked((int)ctx[CpuRegister.Rdx]), + out var position); + + if (result != OrbisGen2Result.ORBIS_GEN2_OK) + { + ctx[CpuRegister.Rax] = ulong.MaxValue; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + ctx[CpuRegister.Rax] = unchecked((ulong)position); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "oib76F-12fk", + ExportName = "sceKernelLseek", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelLseek(CpuContext ctx) + { + var result = KernelLseekCore( + unchecked((int)ctx[CpuRegister.Rdi]), + unchecked((long)ctx[CpuRegister.Rsi]), + unchecked((int)ctx[CpuRegister.Rdx]), + out var position); + + if (result != OrbisGen2Result.ORBIS_GEN2_OK) + { + return (int)result; + } + + ctx[CpuRegister.Rax] = unchecked((ulong)position); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "taRWhTJFTgE", ExportName = "sceKernelGetdirentries", @@ -1474,6 +1519,58 @@ public static class KernelMemoryCompatExports 0); } + private static OrbisGen2Result KernelLseekCore(int fd, long offset, int whence, out long position) + { + position = -1; + + FileStream? stream; + lock (_fdGate) + { + _openFiles.TryGetValue(fd, out stream); + } + + if (stream is null) + { + LogIoTrace("lseek", $"fd:{fd}", $"offset={offset} whence={whence} result=badfd"); + return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + SeekOrigin origin; + switch (whence) + { + case SeekSet: + origin = SeekOrigin.Begin; + break; + case SeekCur: + origin = SeekOrigin.Current; + break; + case SeekEnd: + origin = SeekOrigin.End; + break; + default: + LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid_whence"); + return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + try + { + position = stream.Seek(offset, origin); + } + catch (IOException ex) + { + LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=io_error ex={ex.Message}"); + return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + catch (ArgumentException ex) + { + LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid ex={ex.Message}"); + return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} pos={position}"); + return OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "FxVZqBAA7ks", ExportName = "_write", @@ -3567,13 +3664,13 @@ public static class KernelMemoryCompatExports var devlogAppRoot = ResolveDevlogAppRoot(); if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase)) { - var relative = guestPath["/devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]); return Path.Combine(devlogAppRoot, relative); } if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase)) { - var relative = guestPath["devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]); return Path.Combine(devlogAppRoot, relative); } @@ -3586,7 +3683,7 @@ public static class KernelMemoryCompatExports var temp0Root = ResolveTemp0Root(); if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase)) { - var relative = guestPath["/temp0/".Length..].Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]); return Path.Combine(temp0Root, relative); } @@ -3606,13 +3703,13 @@ public static class KernelMemoryCompatExports if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase)) { - var relative = guestPath["/app0/".Length..].Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath["/app0/".Length..]); return Path.Combine(app0Root, relative); } if (guestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase)) { - var relative = guestPath["app0/".Length..].Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath["app0/".Length..]); return Path.Combine(app0Root, relative); } @@ -3628,6 +3725,14 @@ public static class KernelMemoryCompatExports return guestPath; } + private static string NormalizeMountRelativePath(string relativePath) + { + return relativePath + .TrimStart('/', '\\') + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + } + private static string ResolveDevlogAppRoot() { var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR"); diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs new file mode 100644 index 0000000..a40984f --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -0,0 +1,318 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Text; +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Kernel; + +public static class KernelSemaphoreCompatExports +{ + private const int MaxSemaphoreNameLength = 128; + private static readonly ConcurrentDictionary _semaphores = new(); + private static int _nextSemaphoreHandle = 1; + + private sealed class KernelSemaphoreState + { + public required string Name { get; init; } + public required int InitialCount { get; init; } + public required int MaxCount { get; init; } + public int Count { get; set; } + public int WaitingThreads { get; set; } + public object Gate { get; } = new(); + } + + [SysAbiExport( + Nid = "188x57JYp0g", + ExportName = "sceKernelCreateSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelCreateSema(CpuContext ctx) + { + var semaphoreAddress = ctx[CpuRegister.Rdi]; + var nameAddress = ctx[CpuRegister.Rsi]; + var attr = unchecked((uint)ctx[CpuRegister.Rdx]); + var initialCount = unchecked((int)ctx[CpuRegister.Rcx]); + var maxCount = unchecked((int)ctx[CpuRegister.R8]); + var optionAddress = ctx[CpuRegister.R9]; + + if (semaphoreAddress == 0 || + nameAddress == 0 || + attr > 2 || + initialCount < 0 || + maxCount <= 0 || + initialCount > maxCount || + optionAddress != 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle)); + if (handle == 0) + { + handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle)); + } + + _semaphores[handle] = new KernelSemaphoreState + { + Name = name, + InitialCount = initialCount, + MaxCount = maxCount, + Count = initialCount, + }; + + if (!TryWriteUInt32(ctx, semaphoreAddress, handle)) + { + _semaphores.TryRemove(handle, out _); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + [SysAbiExport( + Nid = "Zxa0VhQVTsk", + ExportName = "sceKernelWaitSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelWaitSema(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var needCount = unchecked((int)ctx[CpuRegister.Rsi]); + var timeoutAddress = ctx[CpuRegister.Rdx]; + + if (!_semaphores.TryGetValue(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + if (needCount < 1 || needCount > semaphore.MaxCount) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + lock (semaphore.Gate) + { + if (semaphore.Count >= needCount) + { + semaphore.Count -= needCount; + TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + if (timeoutAddress != 0) + { + if (!TryReadUInt32(ctx, timeoutAddress, out _)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + _ = TryWriteUInt32(ctx, timeoutAddress, 0); + TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); + } + + if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema")) + { + TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); + } + + semaphore.WaitingThreads++; + TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + } + + [SysAbiExport( + Nid = "12wOHk8ywb0", + ExportName = "sceKernelPollSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelPollSema(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var needCount = unchecked((int)ctx[CpuRegister.Rsi]); + + if (!_semaphores.TryGetValue(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + if (needCount < 1 || needCount > semaphore.MaxCount) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + lock (semaphore.Gate) + { + if (semaphore.Count < needCount) + { + TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + } + + semaphore.Count -= needCount; + TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + } + + [SysAbiExport( + Nid = "4czppHBiriw", + ExportName = "sceKernelSignalSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelSignalSema(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var signalCount = unchecked((int)ctx[CpuRegister.Rsi]); + + if (!_semaphores.TryGetValue(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + if (signalCount <= 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + lock (semaphore.Gate) + { + if (semaphore.Count > semaphore.MaxCount - signalCount) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + semaphore.Count += signalCount; + TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + } + + [SysAbiExport( + Nid = "4DM06U2BNEY", + ExportName = "sceKernelCancelSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelCancelSema(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var setCount = unchecked((int)ctx[CpuRegister.Rsi]); + var waitingThreadsAddress = ctx[CpuRegister.Rdx]; + + if (!_semaphores.TryGetValue(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + if (setCount > semaphore.MaxCount) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + lock (semaphore.Gate) + { + if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads))) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount; + semaphore.WaitingThreads = 0; + TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + } + + [SysAbiExport( + Nid = "R1Jvn8bSCW8", + ExportName = "sceKernelDeleteSema", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelDeleteSema(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + if (!_semaphores.TryRemove(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + private static int SetReturn(CpuContext ctx, OrbisGen2Result result) + { + var value = (int)result; + ctx[CpuRegister.Rax] = unchecked((ulong)value); + return value; + } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } + + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value) + { + value = string.Empty; + if (address == 0 || maxLength <= 0) + { + return false; + } + + var bytes = new byte[Math.Min(maxLength, 4096)]; + for (var i = 0; i < bytes.Length; i++) + { + Span current = stackalloc byte[1]; + if (!ctx.Memory.TryRead(address + (ulong)i, current)) + { + return false; + } + + if (current[0] == 0) + { + value = Encoding.UTF8.GetString(bytes, 0, i); + return true; + } + + bytes[i] = current[0]; + } + + value = Encoding.UTF8.GetString(bytes); + return true; + } + + private static void TraceSemaphore(string message) + { + if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal)) + { + Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}"); + } + } +} diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs new file mode 100644 index 0000000..b80473f --- /dev/null +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -0,0 +1,158 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; +using System.Diagnostics; + +namespace SharpEmu.Libs.Pad; + +public static class PadExports +{ + private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003); + private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005); + private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007); + private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008); + private const int PrimaryUserId = 1; + private const int StandardPortType = 0; + private const int PrimaryPadHandle = 1; + private const int ControllerInformationSize = 0x1C; + private const int PadDataSize = 0x78; + + private static bool _initialized; + + [SysAbiExport( + Nid = "hv1luiJrqQM", + ExportName = "scePadInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadInit(CpuContext ctx) + { + _initialized = true; + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "xk0AcarP3V4", + ExportName = "scePadOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadOpen(CpuContext ctx) + { + var userId = unchecked((int)ctx[CpuRegister.Rdi]); + var type = unchecked((int)ctx[CpuRegister.Rsi]); + var index = unchecked((int)ctx[CpuRegister.Rdx]); + var parameterAddress = ctx[CpuRegister.Rcx]; + if (!_initialized) + { + return SetReturn(ctx, OrbisPadErrorNotInitialized); + } + + if (userId == -1) + { + return SetReturn(ctx, OrbisPadErrorDeviceNoHandle); + } + + if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0) + { + return SetReturn(ctx, OrbisPadErrorDeviceNotConnected); + } + + return SetReturn(ctx, PrimaryPadHandle); + } + + [SysAbiExport( + Nid = "clVvL4ZDntw", + ExportName = "scePadSetMotionSensorState", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadSetMotionSensorState(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + return handle == PrimaryPadHandle + ? SetReturn(ctx, 0) + : SetReturn(ctx, OrbisPadErrorInvalidHandle); + } + + [SysAbiExport( + Nid = "gjP9-KQzoUk", + ExportName = "scePadGetControllerInformation", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadGetControllerInformation(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var informationAddress = ctx[CpuRegister.Rsi]; + if (handle != PrimaryPadHandle) + { + return SetReturn(ctx, OrbisPadErrorInvalidHandle); + } + + if (informationAddress == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + Span information = stackalloc byte[ControllerInformationSize]; + BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f); + BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920); + BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943); + information[0x08] = 30; + information[0x09] = 30; + information[0x0A] = StandardPortType; + information[0x0B] = 1; + information[0x0C] = 1; + BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0); + + return ctx.Memory.TryWrite(informationAddress, information) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + [SysAbiExport( + Nid = "YndgXqQVV7c", + ExportName = "scePadReadState", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadReadState(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var dataAddress = ctx[CpuRegister.Rsi]; + if (handle != PrimaryPadHandle) + { + return SetReturn(ctx, OrbisPadErrorInvalidHandle); + } + + if (dataAddress == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + Span data = stackalloc byte[PadDataSize]; + data.Clear(); + data[0x04] = 128; + data[0x05] = 128; + data[0x06] = 128; + data[0x07] = 128; + BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f); + data[0x4C] = 1; + var timestampTicks = Stopwatch.GetTimestamp(); + var timestampMicroseconds = + ((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) + + ((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency); + BinaryPrimitives.WriteUInt64LittleEndian( + data[0x50..], + timestampMicroseconds); + data[0x68] = 1; + + return ctx.Memory.TryWrite(dataAddress, data) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs new file mode 100644 index 0000000..cb63ffc --- /dev/null +++ b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; + +namespace SharpEmu.Libs.SystemService; + +public static class SystemServiceExports +{ + private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003); + private const int SystemServiceStatusSize = 0x0C; + + [SysAbiExport( + Nid = "rPo6tV8D9bM", + ExportName = "sceSystemServiceGetStatus", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceSystemService")] + public static int SystemServiceGetStatus(CpuContext ctx) + { + var statusAddress = ctx[CpuRegister.Rdi]; + if (statusAddress == 0) + { + return SetReturn(ctx, OrbisSystemServiceErrorParameter); + } + + Span status = stackalloc byte[SystemServiceStatusSize]; + status.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(status, 0); + status[0x06] = 1; + + return ctx.Memory.TryWrite(statusAddress, status) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/UserService/UserServiceExports.cs b/src/SharpEmu.Libs/UserService/UserServiceExports.cs new file mode 100644 index 0000000..1134995 --- /dev/null +++ b/src/SharpEmu.Libs/UserService/UserServiceExports.cs @@ -0,0 +1,79 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; + +namespace SharpEmu.Libs.UserService; + +public static class UserServiceExports +{ + private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005); + private const int PrimaryUserId = 1; + private const int InvalidUserId = -1; + + [SysAbiExport( + Nid = "j3YMu1MVNNo", + ExportName = "sceUserServiceInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceUserService")] + public static int UserServiceInitialize(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "CdWp0oHWGr0", + ExportName = "sceUserServiceGetInitialUser", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceUserService")] + public static int UserServiceGetInitialUser(CpuContext ctx) + { + var userIdAddress = ctx[CpuRegister.Rdi]; + if (userIdAddress == 0) + { + return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); + } + + return TryWriteInt32(ctx, userIdAddress, PrimaryUserId) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + [SysAbiExport( + Nid = "fPhymKNvK-A", + ExportName = "sceUserServiceGetLoginUserIdList", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceUserService")] + public static int UserServiceGetLoginUserIdList(CpuContext ctx) + { + var userIdListAddress = ctx[CpuRegister.Rdi]; + if (userIdListAddress == 0) + { + return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); + } + + Span userIds = stackalloc byte[sizeof(int) * 4]; + BinaryPrimitives.WriteInt32LittleEndian(userIds[0x00..], PrimaryUserId); + BinaryPrimitives.WriteInt32LittleEndian(userIds[0x04..], InvalidUserId); + BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId); + BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId); + return ctx.Memory.TryWrite(userIdListAddress, userIds) + ? 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)]; + BinaryPrimitives.WriteInt32LittleEndian(bytes, value); + return ctx.Memory.TryWrite(address, bytes); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index a834899..d39ccbb 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -15,28 +16,79 @@ public static class VideoOutExports private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009); private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A); private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B); + private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C); + private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D); private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A); private const int SceVideoOutBusTypeMain = 0; private const int SceVideoOutBufferAttributeOptionNone = 0; + private const int SceVideoOutTilingModeLinear = 1; private const int MaxOpenPorts = 4; private const int MaxDisplayBuffers = 16; - private const int VideoOutBufferAttributeSize = 0x24; + private const int MaxDisplayBufferGroups = 4; + private const int MaxFrameDumps = 8; + private const int VideoOutBufferAttributeSize = 0x28; private const int VideoOutBufferAttribute2Size = 0x50; private const int VideoOutBuffersEntrySize = 0x20; + private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; + private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; + private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000; + private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000; + private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000; + private const ulong SceVideoOutInternalEventFlip = 0x6; + private const short OrbisKernelEventFilterVideoOut = -13; private static readonly object _stateGate = new(); + private static readonly object _frameDumpGate = new(); private static readonly Dictionary _ports = new(); + private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new(); private static int _nextHandle = 1; + private static int _frameDumpCount; + private static long _nextFrameDumpIndex; private sealed class VideoOutPortState { public required int Handle { get; init; } public int FlipRate { get; set; } public ulong VblankCount { get; set; } - public int NextSetId { get; set; } = 1; - public HashSet RegisteredSetIds { get; } = new(); + public ulong FlipCount { get; set; } + public int CurrentBuffer { get; set; } = -1; + public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups]; + public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots(); + public List FlipEvents { get; } = new(); } + private sealed class VideoOutBufferGroup + { + public required int Index { get; init; } + public required BufferAttribute Attribute { get; init; } + } + + private sealed class VideoOutBufferSlot + { + public int GroupIndex { get; set; } = -1; + public ulong AddressLeft { get; set; } + public ulong AddressRight { get; set; } + } + + private readonly record struct FlipEventRegistration(ulong Equeue, ulong UserData); + + private readonly record struct BufferAttribute( + ulong PixelFormat, + uint TilingMode, + uint AspectRatio, + uint Width, + uint Height, + uint PitchInPixel, + ulong Option); + + internal readonly record struct DisplayBufferInfo( + ulong Address, + ulong PixelFormat, + uint TilingMode, + uint Width, + uint Height, + uint PitchInPixel); + [SysAbiExport( Nid = "Up36PTk687E", ExportName = "sceVideoOutOpen", @@ -136,6 +188,146 @@ public static class VideoOutExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "HXzjK9yI30k", + ExportName = "sceVideoOutAddFlipEvent", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutAddFlipEvent(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; + } + + lock (_stateGate) + { + port.FlipEvents.Add(new FlipEventRegistration(equeue, userData)); + } + + TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "U46NwOiJpys", + ExportName = "sceVideoOutSubmitFlip", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSubmitFlip(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]); + var flipMode = unchecked((int)ctx[CpuRegister.Rdx]); + var flipArg = unchecked((long)ctx[CpuRegister.Rcx]); + return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg); + } + + [SysAbiExport( + Nid = "U2JJtSqNKZI", + ExportName = "sceVideoOutGetEventId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutGetEventId(CpuContext ctx) + { + var eventAddress = ctx[CpuRegister.Rdi]; + if (eventAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + if (!ctx.TryReadUInt64(eventAddress, out var ident) || + !TryReadInt16(ctx, eventAddress + 0x08, out var filter)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip) + { + return OrbisVideoOutErrorInvalidEvent; + } + + return 0; + } + + [SysAbiExport( + Nid = "rWUTcKdkUzQ", + ExportName = "sceVideoOutGetEventData", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutGetEventData(CpuContext ctx) + { + var eventAddress = ctx[CpuRegister.Rdi]; + var dataAddress = ctx[CpuRegister.Rsi]; + if (eventAddress == 0 || dataAddress == 0) + { + return OrbisVideoOutErrorInvalidAddress; + } + + if (!ctx.TryReadUInt64(eventAddress, out var ident) || + !TryReadInt16(ctx, eventAddress + 0x08, out var filter) || + !ctx.TryReadUInt64(eventAddress + 0x10, out var data)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip) + { + return OrbisVideoOutErrorInvalidEvent; + } + + return ctx.TryWriteUInt64(dataAddress, data >> 16) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) => + SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg); + + internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info) + { + info = default; + if (bufferIndex < 0 || bufferIndex >= MaxDisplayBuffers) + { + return false; + } + + lock (_stateGate) + { + if (!_ports.TryGetValue(handle, out var port)) + { + return false; + } + + var slot = port.BufferSlots[bufferIndex]; + if (slot.AddressLeft == 0 || + slot.GroupIndex < 0 || + slot.GroupIndex >= port.Groups.Length || + port.Groups[slot.GroupIndex] is not { } group) + { + return false; + } + + var attribute = group.Attribute; + info = new DisplayBufferInfo( + slot.AddressLeft, + attribute.PixelFormat, + attribute.TilingMode, + attribute.Width, + attribute.Height, + attribute.PitchInPixel); + return true; + } + } + [SysAbiExport( Nid = "MTxxrOCeSig", ExportName = "sceVideoOutSetWindowModeMargins", @@ -173,9 +365,23 @@ public static class VideoOutExports lock (_stateGate) { - return port.RegisteredSetIds.Remove(attributeIndex) - ? (int)OrbisGen2Result.ORBIS_GEN2_OK - : OrbisVideoOutErrorInvalidValue; + if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null) + { + return OrbisVideoOutErrorInvalidValue; + } + + port.Groups[attributeIndex] = null; + foreach (var slot in port.BufferSlots) + { + if (slot.GroupIndex == attributeIndex) + { + slot.GroupIndex = -1; + slot.AddressLeft = 0; + slot.AddressRight = 0; + } + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } } @@ -292,16 +498,21 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidValue; } + if (!TryReadBufferAttribute(ctx, attributeAddress, false, out var attribute)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + Span addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)]; for (var i = 0; i < bufferNum; i++) { - if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _)) + if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out addresses[i])) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } } - var setId = AllocateBufferSet(port); - return setId; + return RegisterBufferRange(port, startIndex, addresses[..bufferNum], attribute); } [SysAbiExport( @@ -348,34 +559,458 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidValue; } + if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + Span addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)]; for (var i = 0; i < bufferNum; i++) { var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize); - if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) || + if (!ctx.TryReadUInt64(entryAddress + 0x00, out addresses[i]) || !ctx.TryReadUInt64(entryAddress + 0x08, out _)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } } - lock (_stateGate) - { - port.RegisteredSetIds.Add(setIndex); - } - - return setIndex; + var groupIndex = RegisterBufferRange(port, bufferIndexStart, addresses[..bufferNum], attribute, setIndex); + return groupIndex < 0 ? groupIndex : setIndex; } - private static int AllocateBufferSet(VideoOutPortState port) + private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) + { + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (bufferIndex < -1 || bufferIndex >= MaxDisplayBuffers) + { + return OrbisVideoOutErrorInvalidIndex; + } + + ulong eventData; + List flipEvents; + lock (_stateGate) + { + if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0) + { + return OrbisVideoOutErrorInvalidIndex; + } + + port.CurrentBuffer = bufferIndex; + port.FlipCount++; + var eventCount = Math.Min(port.FlipCount, 0xFUL); + var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL; + eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16); + flipEvents = new List(port.FlipEvents); + } + + _ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg); + + foreach (var flipEvent in flipEvents) + { + _ = KernelEventQueueCompatExports.EnqueueEvent( + flipEvent.Equeue, + new KernelEventQueueCompatExports.KernelQueuedEvent( + SceVideoOutInternalEventFlip, + OrbisKernelEventFilterVideoOut, + 0, + 0, + eventData, + flipEvent.UserData)); + } + + TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}"); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan addresses, BufferAttribute attribute, int requestedGroupIndex = -1) { lock (_stateGate) { - var setId = port.NextSetId++; - port.RegisteredSetIds.Add(setId); - return setId; + var groupIndex = requestedGroupIndex >= 0 ? requestedGroupIndex : FindFreeGroupIndex(port); + if (groupIndex < 0 || groupIndex >= MaxDisplayBufferGroups) + { + return OrbisVideoOutErrorInvalidValue; + } + + if (port.Groups[groupIndex] is not null) + { + return OrbisVideoOutErrorResourceBusy; + } + + for (var i = 0; i < addresses.Length; i++) + { + if (port.BufferSlots[startIndex + i].GroupIndex >= 0) + { + return OrbisVideoOutErrorResourceBusy; + } + } + + port.Groups[groupIndex] = new VideoOutBufferGroup + { + Index = groupIndex, + Attribute = attribute, + }; + + for (var i = 0; i < addresses.Length; i++) + { + var slot = port.BufferSlots[startIndex + i]; + slot.GroupIndex = groupIndex; + slot.AddressLeft = addresses[i]; + slot.AddressRight = 0; + } + + 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}"); + return groupIndex; } } + private static int FindFreeGroupIndex(VideoOutPortState port) + { + for (var i = 0; i < port.Groups.Length; i++) + { + if (port.Groups[i] is null) + { + return i; + } + } + + return -1; + } + + private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute) + { + attribute = default; + if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) || + !TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) || + !TryReadUInt32(ctx, attributeAddress + 0x10, out var height)) + { + return false; + } + + if (attribute2) + { + if (!ctx.TryReadUInt64(attributeAddress + 0x18, out var option) || + !ctx.TryReadUInt64(attributeAddress + 0x20, out var pixelFormat)) + { + return false; + } + + attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat), tilingMode, 0, width, height, width, option); + return true; + } + + if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) || + !TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) || + !TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) || + !TryReadUInt32(ctx, attributeAddress + 0x18, out var option32)) + { + return false; + } + + attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat32), tilingMode, aspectRatio, width, height, pitchInPixel, option32); + return true; + } + + private static bool TryDumpFrame(CpuContext ctx, VideoOutPortState port, int bufferIndex, int flipMode, long flipArg) + { + if (bufferIndex < 0) + { + return false; + } + + VideoOutBufferSlot slot; + VideoOutBufferGroup? group; + lock (_stateGate) + { + slot = port.BufferSlots[bufferIndex]; + group = slot.GroupIndex >= 0 && slot.GroupIndex < port.Groups.Length + ? port.Groups[slot.GroupIndex] + : null; + } + + if (group is null || slot.AddressLeft == 0) + { + return false; + } + + var attribute = group.Attribute; + if (attribute.Width == 0 || attribute.Height == 0 || attribute.Width > 8192 || attribute.Height > 8192) + { + return false; + } + + var bytesPerPixel = GetBytesPerPixel(attribute.PixelFormat); + if (bytesPerPixel == 0) + { + return DumpRawFrame(ctx, port.Handle, slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "unsupported-format"); + } + + var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel; + var rowBytes = checked((int)(pitch * bytesPerPixel)); + var visibleRowBytes = checked((int)(attribute.Width * bytesPerPixel)); + var frameBytes = checked((ulong)rowBytes * attribute.Height); + if (frameBytes > 256UL * 1024UL * 1024UL) + { + return false; + } + + lock (_frameDumpGate) + { + if (_frameDumpCount >= MaxFrameDumps) + { + return false; + } + } + + const ulong fnvOffsetBasis = 14695981039346656037UL; + const ulong fnvPrime = 1099511628211UL; + var fingerprint = fnvOffsetBasis; + var row = new byte[rowBytes]; + for (uint y = 0; y < attribute.Height; y++) + { + if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row)) + { + return false; + } + + foreach (var value in row.AsSpan(0, visibleRowBytes)) + { + fingerprint = (fingerprint ^ value) * fnvPrime; + } + } + + var fingerprintKey = (port.Handle, bufferIndex, slot.AddressLeft); + lock (_frameDumpGate) + { + if (_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) && + previousFingerprint == fingerprint) + { + return false; + } + + if (_frameDumpCount >= MaxFrameDumps) + { + return false; + } + + _lastFrameFingerprints[fingerprintKey] = fingerprint; + _frameDumpCount++; + } + + var rgb = new byte[checked((int)(attribute.Width * attribute.Height * 3))]; + var rgbOffset = 0; + for (uint y = 0; y < attribute.Height; y++) + { + if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row)) + { + return false; + } + + ConvertRowToRgb(row.AsSpan(0, visibleRowBytes), rgb.AsSpan(rgbOffset, (int)attribute.Width * 3), attribute.PixelFormat); + rgbOffset += (int)attribute.Width * 3; + } + + var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex); + var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex); + WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb); + WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint); + TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}"); + return true; + } + + private static bool DumpRawFrame(CpuContext ctx, int handle, ulong address, BufferAttribute attribute, int bufferIndex, int flipMode, long flipArg, string reason) + { + var bytesPerPixel = Math.Max(GetBytesPerPixel(attribute.PixelFormat), 4u); + var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel; + var byteCount = checked((ulong)pitch * attribute.Height * bytesPerPixel); + if (byteCount == 0 || byteCount > 256UL * 1024UL * 1024UL) + { + return false; + } + + var bytes = new byte[(int)byteCount]; + if (!ctx.Memory.TryRead(address, bytes)) + { + return false; + } + + var fingerprint = ComputeFingerprint(bytes); + var fingerprintKey = (handle, bufferIndex, address); + lock (_frameDumpGate) + { + if ((_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) && + previousFingerprint == fingerprint) || + _frameDumpCount >= MaxFrameDumps) + { + return false; + } + + _lastFrameFingerprints[fingerprintKey] = fingerprint; + _frameDumpCount++; + } + + var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex); + var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex); + File.WriteAllBytes(basePath + ".raw", bytes); + WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint); + TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}"); + return true; + } + + private static ulong ComputeFingerprint(ReadOnlySpan bytes) + { + const ulong fnvOffsetBasis = 14695981039346656037UL; + const ulong fnvPrime = 1099511628211UL; + var fingerprint = fnvOffsetBasis; + foreach (var value in bytes) + { + fingerprint = (fingerprint ^ value) * fnvPrime; + } + + return fingerprint; + } + + private static uint GetBytesPerPixel(ulong pixelFormat) => + pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or + SceVideoOutPixelFormatA8B8G8R8Srgb or + SceVideoOutPixelFormatA2R10G10B10 or + SceVideoOutPixelFormatA2R10G10B10Srgb or + SceVideoOutPixelFormatA2R10G10B10Bt2020Pq + ? 4u + : 0u; + + private static ulong NormalizePixelFormat(ulong pixelFormat) + { + if (GetBytesPerPixel(pixelFormat) != 0) + { + return pixelFormat; + } + + var low = (uint)(pixelFormat & 0xFFFF_FFFFUL); + if (GetBytesPerPixel(low) != 0) + { + return low; + } + + var high = (uint)(pixelFormat >> 32); + if (GetBytesPerPixel(high) != 0) + { + return high; + } + + var packed = high | (low >> 16); + return GetBytesPerPixel(packed) != 0 ? packed : pixelFormat; + } + + private static void ConvertRowToRgb(ReadOnlySpan source, Span destination, ulong pixelFormat) + { + var dst = 0; + for (var src = 0; src + 3 < source.Length; src += 4) + { + if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb) + { + destination[dst++] = source[src + 0]; + destination[dst++] = source[src + 1]; + destination[dst++] = source[src + 2]; + } + else if (pixelFormat is SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Bt2020Pq) + { + var value = BinaryPrimitives.ReadUInt32LittleEndian(source[src..(src + 4)]); + destination[dst++] = (byte)(((value >> 20) & 0x3FF) >> 2); + destination[dst++] = (byte)(((value >> 10) & 0x3FF) >> 2); + destination[dst++] = (byte)((value & 0x3FF) >> 2); + } + else + { + destination[dst++] = source[src + 2]; + destination[dst++] = source[src + 1]; + destination[dst++] = source[src + 0]; + } + } + } + + private static string GetFrameDumpBasePath(long frameIndex, int handle, int bufferIndex) + { + var directory = GetLogsDirectory(); + Directory.CreateDirectory(directory); + return Path.Combine(directory, $"videoout_frame_{frameIndex:D4}_h{handle}_b{bufferIndex}"); + } + + private static string GetLogsDirectory() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "SharpEmu.slnx"))) + { + return Path.Combine(current.FullName, "logs"); + } + + current = current.Parent; + } + + return Path.Combine(Directory.GetCurrentDirectory(), "logs"); + } + + private static void WriteBmp(string path, uint width, uint height, byte[] rgb) + { + var rowStride = checked((int)(((width * 3u) + 3u) & ~3u)); + var pixelBytes = checked(rowStride * (int)height); + var fileSize = 54 + pixelBytes; + using var stream = File.Create(path); + Span header = stackalloc byte[54]; + header[0] = (byte)'B'; + header[1] = (byte)'M'; + BinaryPrimitives.WriteUInt32LittleEndian(header[0x02..], (uint)fileSize); + BinaryPrimitives.WriteUInt32LittleEndian(header[0x0A..], 54); + BinaryPrimitives.WriteUInt32LittleEndian(header[0x0E..], 40); + BinaryPrimitives.WriteInt32LittleEndian(header[0x12..], (int)width); + BinaryPrimitives.WriteInt32LittleEndian(header[0x16..], -(int)height); + BinaryPrimitives.WriteUInt16LittleEndian(header[0x1A..], 1); + BinaryPrimitives.WriteUInt16LittleEndian(header[0x1C..], 24); + BinaryPrimitives.WriteUInt32LittleEndian(header[0x22..], (uint)pixelBytes); + stream.Write(header); + + var row = new byte[rowStride]; + var sourceStride = (int)width * 3; + var heightInt = (int)height; + var widthInt = (int)width; + for (var y = 0; y < heightInt; y++) + { + row.AsSpan().Clear(); + var src = rgb.AsSpan(y * sourceStride, sourceStride); + for (var x = 0; x < widthInt; x++) + { + row[(x * 3) + 0] = src[(x * 3) + 2]; + row[(x * 3) + 1] = src[(x * 3) + 1]; + row[(x * 3) + 2] = src[(x * 3) + 0]; + } + + stream.Write(row); + } + } + + private static void WriteFrameMetadata( + string path, + ulong address, + BufferAttribute attribute, + int bufferIndex, + int flipMode, + long flipArg, + string kind, + ulong fingerprint) + { + File.WriteAllText( + path, + $"kind={kind}\naddress=0x{address:X16}\nbuffer_index={bufferIndex}\nflip_mode={flipMode}\nflip_arg={flipArg}\nfingerprint=0x{fingerprint:X16}\npixel_format=0x{attribute.PixelFormat:X}\ntiling_mode={attribute.TilingMode}\nwidth={attribute.Width}\nheight={attribute.Height}\npitch_in_pixel={attribute.PitchInPixel}\noption=0x{attribute.Option:X}\n"); + } + private static bool IsValidBufferRange(int startIndex, int bufferNum) { return startIndex >= 0 && @@ -393,6 +1028,17 @@ public static class VideoOutExports } } + private static VideoOutBufferSlot[] CreateBufferSlots() + { + var slots = new VideoOutBufferSlot[MaxDisplayBuffers]; + for (var i = 0; i < slots.Length; i++) + { + slots[i] = new VideoOutBufferSlot(); + } + + return slots; + } + private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value) { var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08); @@ -406,4 +1052,40 @@ public static class VideoOutExports value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); return true; } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } + + private static bool TryReadInt16(CpuContext ctx, ulong address, out short value) + { + Span buffer = stackalloc byte[sizeof(short)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadInt16LittleEndian(buffer); + return true; + } + + private static void TraceVideoOut(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message}"); + } }