diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index 09fb5b2..3e4297f 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -362,7 +362,7 @@ public static class KernelExports ExportName = "open", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx); + public static int Open(CpuContext ctx) => KernelMemoryCompatExports.PosixOpen(ctx); [SysAbiExport( Nid = "1G3lF1Gg1k8", @@ -376,7 +376,7 @@ public static class KernelExports ExportName = "fstat", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] - public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx); + public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.PosixFstat(ctx); [SysAbiExport( Nid = "hcuQgD53UxM", diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 17f4288..80e7e42 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -65,7 +65,10 @@ public static partial class KernelMemoryCompatExports private const uint HostPageExecuteReadWrite = 0x40; private const uint HostPageExecuteWriteCopy = 0x80; private const uint HostPageGuard = 0x100; + private const int Enoent = 2; + private const int Ebadf = 9; private const int Enomem = 12; + private const int Eacces = 13; private const int Efault = 14; private const int Einval = 22; private const int Erange = 34; @@ -1545,7 +1548,13 @@ public static partial class KernelMemoryCompatExports ExportName = "close", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + public static int PosixClose(CpuContext ctx) + { + var result = KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi])); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport( Nid = "UK2Tl2DWUns", @@ -1602,6 +1611,29 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + // Translates a failed raw Orbis kernel result into the libc/POSIX ABI: + // return -1 with errno set. The raw sceKernel* implementations report the + // 0x8002xxxx sentinel through the return value, but the POSIX-named exports + // (open/fstat/close/read/write/stat) are called by libc code that expects a + // negative result on failure. Leaking the raw sentinel makes callers store + // it as a "valid" fd or handle and later dereference it - the null-pointer + // access violation seen when Unity's IL2CPP file layer probes an absent + // il2cpp.usym. fd-based calls pass notFoundErrno=Ebadf; path-based calls + // leave the Enoent default. + private static int PosixFailure(CpuContext ctx, int orbisResult, int notFoundErrno = Enoent) + { + var errno = orbisResult switch + { + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => Eacces, + _ => notFoundErrno, + }; + KernelRuntimeCompatExports.TrySetErrno(ctx, errno); + ctx[CpuRegister.Rax] = ulong.MaxValue; + return -1; + } + [SysAbiExport( Nid = "E6ao34wPw+U", ExportName = "stat", @@ -1610,23 +1642,29 @@ public static partial class KernelMemoryCompatExports public static int PosixStat(CpuContext ctx) { var result = KernelStat(ctx); - if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK) - { - return 0; - } + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result); + } - // stat(2) follows the libc/POSIX ABI: failures return -1 and expose - // the reason through errno. Returning the raw Orbis kernel code here - // makes callers treat a missing file as a non-negative success value. - var errno = result switch - { - (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval, - (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault, - _ => 2, // ENOENT - }; - KernelRuntimeCompatExports.TrySetErrno(ctx, errno); - ctx[CpuRegister.Rax] = ulong.MaxValue; - return -1; + // POSIX open(2): translates a failed raw open into -1/errno. On success + // KernelOpenUnderscore already writes the fd into RAX (the import bridge + // prefers a written RAX over the return value), so returning 0 is correct. + public static int PosixOpen(CpuContext ctx) + { + var result = KernelOpenUnderscore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result); + } + + // POSIX fstat(2): a bad fd maps to EBADF rather than the path-oriented ENOENT. + public static int PosixFstat(CpuContext ctx) + { + var result = KernelFstat(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); } [SysAbiExport( @@ -2096,7 +2134,15 @@ public static partial class KernelMemoryCompatExports ExportName = "read", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx); + public static int PosixRead(CpuContext ctx) + { + // On success KernelReadUnderscore writes the byte count into RAX, which + // the import bridge prefers over this return value. + var result = KernelReadUnderscore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport( Nid = "Cg4srZ6TKbU", @@ -2295,7 +2341,15 @@ public static partial class KernelMemoryCompatExports ExportName = "write", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx); + public static int PosixWrite(CpuContext ctx) + { + // On success KernelWriteUnderscore writes the byte count into RAX, which + // the import bridge prefers over this return value. + var result = KernelWriteUnderscore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport( Nid = "4wSze92BhLI", diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index e90ecfc..2769e0d 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -5290,10 +5290,20 @@ public static partial class Gen5SpirvTranslator UInt(0x108)); } + // A wave-mask SGPR (VCC/EXEC) consumed as a per-lane predicate — the + // condition of VCndmask, a VCC/EXEC branch, or the derived _vcc/_exec + // bool — must be tested at the CURRENT lane's bit, exactly as the + // hardware does, not as "the 64-bit value is non-zero". The two coincide + // for comparison results (only the lane's own bit is ever set), so the + // single-lane path historically used a cheaper whole-word non-zero test. + // But bitwise-complement wave-mask idioms (S_NOT/S_ORN2/S_ANDN2/S_NAND/ + // S_NOR on a 64-bit mask) set the unused upper 63 bits; a whole-word test + // then reports "lane active" even when this lane's bit is clear. Unity's + // PostProcessing NaN killer does exactly this (`anyNaN | ~allFinite`), + // which made every valid pixel read as NaN and get replaced with 0 — + // zeroing the whole scene before tonemap. Extract the lane bit always. private uint IsWaveMaskActive(uint mask) => - _subgroupInvocationIdInput == 0 - ? IsNotZero64(mask) - : IsCurrentLaneSet(mask); + IsCurrentLaneSet(mask); private uint IsCurrentLaneSet(uint mask) => IsNotZero64( diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs new file mode 100644 index 0000000..56fd7fe --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +// Regression tests for how a VCC/EXEC wave mask consumed as a per-lane predicate +// is lowered to SPIR-V. A wave mask must be tested at the current lane's bit +// (mask & lane_bit) — exactly as the hardware evaluates the VCndmask condition or +// a VCC/EXEC branch — not with a whole-word "the 64-bit value is non-zero" test. +// +// The two agree for comparison results (only the lane's own bit is ever set), but +// diverge for the bitwise-complement wave-mask idioms (S_NOT / S_ORN2 / S_ANDN2 / +// S_NAND / S_NOR), which set the unused upper 63 bits. A whole-word test then +// reports the lane active even when its bit is clear. Unity's PostProcessing NaN +// killer combines its channels as `anyNaN | ~allFinite` (S_ORN2_B64); under the +// whole-word test every valid pixel read as NaN and was replaced with 0, zeroing +// the whole HDR scene before tone-mapping. +public sealed class Gen5WaveMaskSpirvTests +{ + private const ulong ShaderAddress = 0x1_0000_0000; + + [Fact] + public void WaveMaskPredicate_IsTestedAtCurrentLaneBit() + { + // V_CMP_EQ_F32 vcc, v0, v1 writes VCC at run time, which re-materialises + // the per-lane _vcc predicate from the wave mask via IsWaveMaskActive. + var spirv = Compile([0x7C04_0300u]); + + // The lane's bit in single-lane emulation is the 64-bit constant 1, so the + // predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0` + // with no such mask. Require the lane-bit AND to be present. + Assert.True( + ContainsLaneBitMaskedWaveTest(spirv), + "wave-mask predicate must be tested at the current lane bit " + + "(mask & lane_bit), not as a whole-word non-zero test"); + } + + // True when the module contains an OpBitwiseAnd whose operand is a 64-bit + // constant of value 1 — the current-lane bit that IsCurrentLaneSet masks the + // wave mask with before the non-zero test. + private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv) + { + var laneBitConstIds = new HashSet(); + + // Pass 1: collect 64-bit OpConstant result-ids whose value is 1. + foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) + { + // OpConstant = 43; a 64-bit constant occupies 5 words + // (opcode, resultType, resultId, valueLow, valueHigh). + if (op != 43 || wordCount != 5) + { + continue; + } + + var resultId = ReadWord(spirv, offset + 8); + var low = ReadWord(spirv, offset + 12); + var high = ReadWord(spirv, offset + 16); + if (low == 1 && high == 0) + { + laneBitConstIds.Add(resultId); + } + } + + // Pass 2: look for an OpBitwiseAnd that consumes one of those constants. + foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) + { + // OpBitwiseAnd = 199 (opcode, resultType, resultId, operand0, operand1). + if (op != 199 || wordCount != 5) + { + continue; + } + + var operand0 = ReadWord(spirv, offset + 12); + var operand1 = ReadWord(spirv, offset + 16); + if (laneBitConstIds.Contains(operand0) || laneBitConstIds.Contains(operand1)) + { + return true; + } + } + + return false; + } + + private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions( + byte[] spirv) + { + // 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions. + for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;) + { + var word = ReadWord(spirv, offset); + var wordCount = (int)(word >> 16); + if (wordCount <= 0) + { + yield break; + } + + yield return ((ushort)word, wordCount, offset); + offset += wordCount * sizeof(uint); + } + } + + private static uint ReadWord(byte[] spirv, int offset) => + BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint))); + + private static byte[] Compile(uint[] programWords) + { + var memory = new FakeCpuMemory(ShaderAddress, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords); + var shaderRegisters = new Dictionary + { + [Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1, + }; + + Assert.True( + Gen5ShaderTranslator.TryCreateState( + ctx, + ShaderAddress, + 0, + shaderRegisters, + Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister, + out var state, + out var error), + error); + Assert.True( + Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error), + error); + Assert.True( + Gen5SpirvTranslator.TryCompileComputeShader( + state, evaluation, 1, 1, 1, out var shader, out error), + error); + return shader.Spirv; + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs index c7a5058..203b970 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs @@ -41,6 +41,90 @@ public sealed class KernelMemoryCompatExportsTests Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); } + [Fact] + public void PosixOpen_MissingFileReturnsMinusOne() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong pathAddress = memoryBase + 0x100; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/il2cpp.usym"); + context[CpuRegister.Rdi] = pathAddress; + context[CpuRegister.Rsi] = 0; // O_RDONLY + + var result = KernelMemoryCompatExports.PosixOpen(context); + + // A libc open() failure must be -1, not the raw 0x8002xxxx sentinel the + // guest would otherwise store as a valid fd and later dereference. + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + } + + [Fact] + public void PosixFstat_BadDescriptorReturnsMinusOne() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong statAddress = memoryBase + 0x400; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0x80020002; // the not-found sentinel misused as an fd + context[CpuRegister.Rsi] = statAddress; + + var result = KernelMemoryCompatExports.PosixFstat(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + } + + [Fact] + public void PosixClose_BadDescriptorReturnsMinusOne() + { + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd + + var result = KernelMemoryCompatExports.PosixClose(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + } + + [Fact] + public void PosixRead_BadDescriptorReturnsMinusOne() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong bufferAddress = memoryBase + 0x200; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd + context[CpuRegister.Rsi] = bufferAddress; + context[CpuRegister.Rdx] = 0x40; + + var result = KernelMemoryCompatExports.PosixRead(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + } + + [Fact] + public void PosixWrite_BadDescriptorReturnsMinusOne() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong bufferAddress = memoryBase + 0x200; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + memory.WriteCString(bufferAddress, "payload"); + context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd + context[CpuRegister.Rsi] = bufferAddress; + context[CpuRegister.Rdx] = 0x7; + + var result = KernelMemoryCompatExports.PosixWrite(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + } + [Fact] public void Sprintf_ReadsVariadicDoubleFromXmmRegister() { diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelSocketCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelSocketCompatExportsTests.cs index fb7a511..5865b84 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelSocketCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelSocketCompatExportsTests.cs @@ -37,10 +37,11 @@ public sealed class KernelSocketCompatExportsTests KernelMemoryCompatExports.PosixClose(context)); Assert.Equal(0UL, context[CpuRegister.Rax]); + // A second close of an already-closed fd fails per the POSIX ABI: + // -1 with errno set, not the raw Orbis NOT_FOUND sentinel. context[CpuRegister.Rdi] = unchecked((ulong)guestFd); - Assert.Equal( - (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, - KernelMemoryCompatExports.PosixClose(context)); + Assert.Equal(-1, KernelMemoryCompatExports.PosixClose(context)); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); } finally {