mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4373f8102e | |||
| 22a38f8849 | |||
| 39387fc6e4 |
@@ -1,27 +1,20 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
## Before submitting
|
||||
|
||||
Please read our contribution guidelines before opening a pull request:
|
||||
|
||||
➡️ [**CONTRIBUTING.md**](https://github.com/sharpemu/sharpemu/blob/main/CONTRIBUTING.md)
|
||||
➡️ [**CONTRIBUTING.md**](CONTRIBUTING.md)
|
||||
|
||||
By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines.
|
||||
|
||||
## Testing
|
||||
|
||||
If applicable, list the game(s) you tested and briefly describe the results.
|
||||
|
||||
Example:
|
||||
|
||||
- Demon's Souls (PPSA01341) – Boots to splash screen.
|
||||
- Dreaming Sarah – Save/load works correctly.
|
||||
|
||||
If your changes do not affect runtime behavior (e.g. documentation, tooling, CI), write `N/A`.
|
||||
|
||||
## Checklist
|
||||
By submitting this PR, you confirm that:
|
||||
|
||||
By submitting this pull request, you confirm that:
|
||||
|
||||
- [ ] I have read and followed `CONTRIBUTING.md`.
|
||||
- [ ] I tested my changes or marked the testing section as `N/A`.
|
||||
- [ ] I wrote this pull request description myself and did not paste AI-generated explanations.
|
||||
- [ ] I listed the game(s) I tested (or marked the testing section as `N/A`).
|
||||
- [ ] I have read `CONTRIBUTING.md`.
|
||||
- [ ] I tested my changes.
|
||||
- [ ] I wrote this description myself and did not copy AI-generated explanations.
|
||||
- [ ] I listed the game(s) I tested, if applicable.
|
||||
|
||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2-beta.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
@@ -10,7 +10,6 @@ path = [
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**",
|
||||
".github/pull_request_template.md",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
|
||||
@@ -16,9 +16,7 @@ namespace SharpEmu.Core.Loader;
|
||||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private const uint ElfMagic = 0x7F454C46;
|
||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||
@@ -357,11 +355,10 @@ public sealed class SelfLoader : ISelfLoader
|
||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||
}
|
||||
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
||||
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||
{
|
||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||
if (!selfHeader.HasKnownLayout)
|
||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||
{
|
||||
throw new InvalidDataException("SELF header signature is not recognized.");
|
||||
}
|
||||
@@ -384,11 +381,13 @@ public sealed class SelfLoader : ISelfLoader
|
||||
// acceptable here; anything else — most commonly a still-encrypted
|
||||
// retail eboot — must be reported clearly rather than failing later
|
||||
// with an opaque "not a valid ELF header" message.
|
||||
if (magic != ElfMagic)
|
||||
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
|
||||
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||
if (leadingWord != ElfMagicBigEndian)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
|
||||
$"(leading bytes 0x{magic:X8}). This is almost certainly a still-encrypted " +
|
||||
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
|
||||
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
|
||||
$"fake-signed (fSELF) image.");
|
||||
}
|
||||
@@ -2790,27 +2789,28 @@ public sealed class SelfLoader : ISelfLoader
|
||||
private readonly ushort _size2;
|
||||
private readonly ulong _fileSize;
|
||||
private readonly ushort _segmentCount;
|
||||
private readonly ushort _flags;
|
||||
private readonly ushort _unknown;
|
||||
private readonly uint _padding;
|
||||
|
||||
public ushort SegmentCount => _segmentCount;
|
||||
|
||||
public ushort Unknown => _unknown;
|
||||
|
||||
public ulong FileSize => _fileSize;
|
||||
|
||||
// Version, key type, and flags are signing metadata and vary across
|
||||
// valid SELF images. They do not change the fixed header layout.
|
||||
public bool HasKnownLayout =>
|
||||
((_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D) ||
|
||||
(_ident0 == 0x54 &&
|
||||
_ident1 == 0x14 &&
|
||||
_ident2 == 0xF5 &&
|
||||
_ident3 == 0xEE)) &&
|
||||
_ident0 == 0x4F &&
|
||||
_ident1 == 0x15 &&
|
||||
_ident2 == 0x3D &&
|
||||
_ident3 == 0x1D &&
|
||||
_ident4 == 0x00 &&
|
||||
_ident5 == 0x01 &&
|
||||
_ident6 == 0x01 &&
|
||||
_ident7 == 0x12;
|
||||
_ident7 == 0x12 &&
|
||||
_ident8 == 0x01 &&
|
||||
_ident9 == 0x01 &&
|
||||
_ident10 == 0x00 &&
|
||||
_ident11 == 0x00;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
|
||||
@@ -295,10 +295,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static bool _presenterCloseRequested;
|
||||
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
||||
private const uint NvidiaVendorId = 0x10DE;
|
||||
private const uint AmdVendorId = 0x1002;
|
||||
// Other GPU PCI vendor IDs, for reference when adding future rules:
|
||||
// Intel 0x8086, Apple 0x106B, Qualcomm 0x5143 (Windows-on-ARM), Microsoft software 0x1414.
|
||||
private const int LastResortPenalty = 1000;
|
||||
private const string PortabilityEnumerationExtensionName = "VK_KHR_portability_enumeration";
|
||||
private const string PortabilitySubsetExtensionName = "VK_KHR_portability_subset";
|
||||
private const int GlfwPlatformHint = 0x00050003;
|
||||
@@ -307,50 +303,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private const int GlfwPlatformWayland = 0x00060003;
|
||||
private const int GlfwPlatformX11 = 0x00060004;
|
||||
private const int GlfwPlatformNull = 0x00060005;
|
||||
|
||||
internal static int ScorePhysicalDevice(
|
||||
PhysicalDeviceProperties properties,
|
||||
string name,
|
||||
string? deviceOverride)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceOverride))
|
||||
{
|
||||
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
|
||||
}
|
||||
|
||||
var score = properties.DeviceType switch
|
||||
{
|
||||
PhysicalDeviceType.DiscreteGpu => 300,
|
||||
PhysicalDeviceType.VirtualGpu => 100,
|
||||
PhysicalDeviceType.IntegratedGpu => 50,
|
||||
PhysicalDeviceType.Cpu => 20,
|
||||
_ => 10,
|
||||
};
|
||||
|
||||
if (properties.VendorID == NvidiaVendorId)
|
||||
{
|
||||
score += 500;
|
||||
}
|
||||
|
||||
score -= ComputeDevicePenalty(properties, OperatingSystem.IsWindows());
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
internal static int ComputeDevicePenalty(PhysicalDeviceProperties properties, bool isWindows)
|
||||
{
|
||||
var penalty = 0;
|
||||
|
||||
if (isWindows &&
|
||||
properties.DeviceType == PhysicalDeviceType.IntegratedGpu &&
|
||||
properties.VendorID == AmdVendorId)
|
||||
{
|
||||
penalty += LastResortPenalty;
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
private static bool _splashHidden;
|
||||
private static long _enqueuedGuestWorkSequence;
|
||||
// Largest contiguous completed sequence, retained for compact diagnostics.
|
||||
@@ -3566,6 +3518,33 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScorePhysicalDevice(
|
||||
PhysicalDeviceProperties properties,
|
||||
string name,
|
||||
string? deviceOverride)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceOverride))
|
||||
{
|
||||
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
|
||||
}
|
||||
|
||||
var score = properties.DeviceType switch
|
||||
{
|
||||
PhysicalDeviceType.DiscreteGpu => 300,
|
||||
PhysicalDeviceType.VirtualGpu => 100,
|
||||
PhysicalDeviceType.Cpu => 50,
|
||||
PhysicalDeviceType.IntegratedGpu => -100,
|
||||
_ => 10,
|
||||
};
|
||||
|
||||
if (properties.VendorID == NvidiaVendorId)
|
||||
{
|
||||
score += 500;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private void LoadComputeDeviceLimits()
|
||||
{
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
|
||||
|
||||
@@ -948,27 +948,6 @@ public static partial class Gen5SpirvTranslator
|
||||
vector);
|
||||
break;
|
||||
}
|
||||
|
||||
case "VPkAddF16":
|
||||
case "VPkMulF16":
|
||||
case "VPkMinF16":
|
||||
case "VPkMaxF16":
|
||||
if (!TryEmitPackedF16(instruction, out result, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case "VPkFmaF16":
|
||||
// Deliberately loud: a fused f16 FMA rounds the product+add once,
|
||||
// whereas doing the multiply-add in f32 and rounding to f16 at the
|
||||
// end double-rounds. Concrete miss: fma(0x4100, 0x7522, 0x04EA) is
|
||||
// 0x7A6B fused but 0x7A6A via f32. Exact emulation (round-to-odd
|
||||
// f32 product then RNE pack) is a planned follow-up slice.
|
||||
error =
|
||||
$"unsupported vop3p opcode {instruction.Opcode} " +
|
||||
"(fused f16 FMA requires single-rounding; deferred to a later slice)";
|
||||
return false;
|
||||
default:
|
||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||
return false;
|
||||
@@ -997,213 +976,6 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
||||
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
||||
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
||||
// GLSL UnpackHalf2x16 / PackHalf2x16, whose subnormal and rounding behaviour
|
||||
// is implementation-defined without float-controls execution modes. The two
|
||||
// lanes are computed independently: each operand half is widened exactly to
|
||||
// f32, op_sel/op_sel_hi pick the source half and neg_lo/neg_hi negate it, the
|
||||
// op runs in f32, and the result is rounded back to f16 with round-to-nearest-
|
||||
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
|
||||
// rounds losslessly to f16 by the double-rounding theorem; a f16 product even
|
||||
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
|
||||
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
|
||||
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
|
||||
private bool TryEmitPackedF16(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out uint result,
|
||||
out string error)
|
||||
{
|
||||
result = 0;
|
||||
error = string.Empty;
|
||||
if (instruction.Control is not Gen5Vop3pControl control)
|
||||
{
|
||||
error = $"missing vop3p control for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (control.Clamp)
|
||||
{
|
||||
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < 2; index++)
|
||||
{
|
||||
var source = instruction.Sources[index];
|
||||
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
||||
{
|
||||
error =
|
||||
$"unsupported vop3p operand {source} for {instruction.Opcode} (first slice: registers only)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var low = EmitPackedF16Lane(instruction, control, highLane: false);
|
||||
var high = EmitPackedF16Lane(instruction, control, highLane: true);
|
||||
result = BitwiseOr(low, ShiftLeftLogical(high, UInt(16)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
||||
private uint EmitPackedF16Lane(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
bool highLane)
|
||||
{
|
||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
||||
var value = instruction.Opcode switch
|
||||
{
|
||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
||||
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
||||
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
||||
_ => left,
|
||||
};
|
||||
return EmitFloatToHalf(Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
|
||||
// widens it exactly to f32 and applies the lane's negate modifier (neg_lo / neg_hi).
|
||||
private uint EmitPackedF16Operand(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
int index,
|
||||
bool highLane)
|
||||
{
|
||||
var raw = GetRawSource(instruction, index);
|
||||
var selectMask = highLane ? control.OpSelHiMask : control.OpSelMask;
|
||||
var half = ((selectMask >> index) & 1) != 0
|
||||
? ShiftRightLogical(raw, UInt(16))
|
||||
: raw;
|
||||
var value = Bitcast(_floatType, EmitHalfToFloat(half));
|
||||
var negateMask = highLane ? control.NegHiMask : control.NegLoMask;
|
||||
if (((negateMask >> index) & 1) != 0)
|
||||
{
|
||||
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// fminnum_like / fmaxnum_like: if one operand is NaN return the other; if both
|
||||
// are NaN return a NaN; otherwise the ordered smaller/larger. The ordering of
|
||||
// -0/+0 is unspecified under these opcodes, so the ordered compare is enough.
|
||||
private uint EmitPackedF16MinMax(uint left, uint right, bool isMax)
|
||||
{
|
||||
var compare = _module.AddInstruction(
|
||||
isMax ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan,
|
||||
_boolType,
|
||||
left,
|
||||
right);
|
||||
var numeric = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, compare, left, right);
|
||||
var leftNan = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
|
||||
var rightNan = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
|
||||
var withRight = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, rightNan, left, numeric);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, leftNan, right, withRight);
|
||||
}
|
||||
|
||||
// Widens an f16 value held in the low 16 bits of `halfBits` to an f32 bit
|
||||
// pattern, exactly (subnormals normalised, Inf/NaN and signed zero preserved).
|
||||
// Mirrors the branchless HalfToFloat reference validated against System.Half.
|
||||
private uint EmitHalfToFloat(uint halfBits)
|
||||
{
|
||||
var sign = ShiftLeftLogical(BitwiseAnd(halfBits, UInt(0x8000)), UInt(16));
|
||||
var exponent = BitwiseAnd(ShiftRightLogical(halfBits, UInt(10)), UInt(0x1F));
|
||||
var mantissa = BitwiseAnd(halfBits, UInt(0x3FF));
|
||||
|
||||
var normal = BitwiseOr(
|
||||
ShiftLeftLogical(IAdd(exponent, UInt(112)), UInt(23)),
|
||||
ShiftLeftLogical(mantissa, UInt(13)));
|
||||
var infinityNan = BitwiseOr(UInt(0x7F80_0000), ShiftLeftLogical(mantissa, UInt(13)));
|
||||
|
||||
// Subnormal: normalise the mantissa. FindUMsb of (mantissa | 1) keeps the
|
||||
// op defined when mantissa is 0; that lane is discarded by the select below.
|
||||
var highBit = Ext(75, _uintType, BitwiseOr(mantissa, UInt(1)));
|
||||
var shift = ISubU(UInt(23), highBit);
|
||||
var subFraction = BitwiseAnd(ShiftLeftLogical(mantissa, shift), UInt(0x7F_FFFF));
|
||||
var subnormal = SelectU(
|
||||
IsNotZero(mantissa),
|
||||
BitwiseOr(ShiftLeftLogical(IAdd(highBit, UInt(103)), UInt(23)), subFraction),
|
||||
UInt(0));
|
||||
|
||||
var magnitude = SelectU(
|
||||
Equal(exponent, 0),
|
||||
subnormal,
|
||||
SelectU(Equal(exponent, 31), infinityNan, normal));
|
||||
return BitwiseOr(sign, magnitude);
|
||||
}
|
||||
|
||||
// Narrows an f32 bit pattern to an f16 value in the low 16 bits, rounding to
|
||||
// nearest even (subnormals, overflow-to-Inf and NaN/Inf handled). Mirrors the
|
||||
// branchless FloatToHalf reference validated exhaustively against System.Half.
|
||||
private uint EmitFloatToHalf(uint bits)
|
||||
{
|
||||
var sign = BitwiseAnd(ShiftRightLogical(bits, UInt(16)), UInt(0x8000));
|
||||
var absolute = BitwiseAnd(bits, UInt(0x7FFF_FFFF));
|
||||
|
||||
var isInfinityNan = UCmp(SpirvOp.UGreaterThanEqual, absolute, UInt(0x7F80_0000));
|
||||
var isNan = UCmp(SpirvOp.UGreaterThan, absolute, UInt(0x7F80_0000));
|
||||
var infinityNan = BitwiseOr(
|
||||
BitwiseOr(sign, UInt(0x7C00)),
|
||||
SelectU(isNan, UInt(0x200), UInt(0)));
|
||||
|
||||
var exponent = ShiftRightLogical(absolute, UInt(23));
|
||||
var mantissa = BitwiseAnd(absolute, UInt(0x7F_FFFF));
|
||||
var significand = BitwiseOr(mantissa, UInt(0x80_0000));
|
||||
|
||||
// Normal path: round the 24-bit significand down to 11 bits (>> 13) with
|
||||
// round-to-nearest-even; the carry folds naturally into the exponent.
|
||||
var roundBit = BitwiseAnd(ShiftRightLogical(significand, UInt(13)), UInt(1));
|
||||
var rounded = ShiftRightLogical(IAdd(IAdd(significand, UInt(0xFFF)), roundBit), UInt(13));
|
||||
var halfExponent = ISubU(exponent, UInt(112));
|
||||
var normalBits = IAdd(ShiftLeftLogical(halfExponent, UInt(10)), ISubU(rounded, UInt(0x400)));
|
||||
var normal = SelectU(
|
||||
UCmp(SpirvOp.UGreaterThanEqual, exponent, UInt(113)),
|
||||
SelectU(UCmp(SpirvOp.UGreaterThanEqual, normalBits, UInt(0x7C00)), UInt(0x7C00), normalBits),
|
||||
UInt(0));
|
||||
|
||||
// Subnormal path: value = round(significand >> (126 - exponent)) with RNE.
|
||||
// The shift is clamped to 25 so it stays defined; on this path it is >= 14.
|
||||
var distance = ISubU(UInt(126), exponent);
|
||||
var shift = SelectU(UCmp(SpirvOp.UGreaterThan, distance, UInt(25)), UInt(25), distance);
|
||||
var shiftMask = ISubU(ShiftLeftLogical(UInt(1), shift), UInt(1));
|
||||
var halfWay = ShiftLeftLogical(UInt(1), ISubU(shift, UInt(1)));
|
||||
var lowBits = BitwiseAnd(significand, shiftMask);
|
||||
var quotient = ShiftRightLogical(significand, shift);
|
||||
var roundUp = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
UCmp(SpirvOp.UGreaterThan, lowBits, halfWay),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
Equal(lowBits, halfWay),
|
||||
IsNotZero(BitwiseAnd(quotient, UInt(1)))));
|
||||
var subnormal = IAdd(quotient, SelectU(roundUp, UInt(1), UInt(0)));
|
||||
|
||||
var isSubnormal = UCmp(SpirvOp.ULessThanEqual, exponent, UInt(112));
|
||||
var finite = SelectU(isSubnormal, subnormal, normal);
|
||||
return SelectU(isInfinityNan, infinityNan, BitwiseOr(sign, finite));
|
||||
}
|
||||
|
||||
private uint SelectU(uint condition, uint whenTrue, uint whenFalse) =>
|
||||
_module.AddInstruction(SpirvOp.Select, _uintType, condition, whenTrue, whenFalse);
|
||||
|
||||
private uint UCmp(SpirvOp operation, uint left, uint right) =>
|
||||
_module.AddInstruction(operation, _boolType, left, right);
|
||||
|
||||
private uint Equal(uint value, uint constant) =>
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, value, UInt(constant));
|
||||
|
||||
private uint ISubU(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.ISub, _uintType, left, right);
|
||||
|
||||
private bool TryEmitVectorCompare(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out string error)
|
||||
|
||||
@@ -237,17 +237,6 @@ public sealed record Gen5SdwaControl(
|
||||
bool Clamp,
|
||||
uint? ScalarDestination) : Gen5InstructionControl;
|
||||
|
||||
// Packed (VOP3P) source and destination modifiers. Each mask holds one bit per
|
||||
// source operand. OpSel/OpSelHi pick which 16-bit half of a source feeds the low
|
||||
// and high result lanes respectively; NegLo/NegHi negate the value routed to each
|
||||
// lane. Clamp saturates each output half to [0, 1].
|
||||
public sealed record Gen5Vop3pControl(
|
||||
uint OpSelMask,
|
||||
uint OpSelHiMask,
|
||||
uint NegLoMask,
|
||||
uint NegHiMask,
|
||||
bool Clamp) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5DppControl(
|
||||
uint Control,
|
||||
bool FetchInactive,
|
||||
|
||||
@@ -562,22 +562,6 @@ public static class Gen5ShaderTranslator
|
||||
return DecodeSop(word, out name, out sizeDwords, out error);
|
||||
}
|
||||
|
||||
// gfx10 moved VOP3P (packed 16-bit math) to its own 0b110011000 prefix
|
||||
// (word0 top byte 0xCC), separate from the VOP3 block. Match the full
|
||||
// 9-bit prefix here, before the coarse major-opcode switch, so packed
|
||||
// instructions are not misread as one of the neighbouring encodings.
|
||||
if ((word & 0xFF800000u) == 0xCC000000u)
|
||||
{
|
||||
encoding = Gen5ShaderEncoding.Vop3p;
|
||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3pExtra))
|
||||
{
|
||||
error = $"vop3p-extra-read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return DecodeVop3p(word, vop3pExtra, out name, out sizeDwords, out error);
|
||||
}
|
||||
|
||||
switch (word >> 26)
|
||||
{
|
||||
case 0x33:
|
||||
@@ -1176,37 +1160,6 @@ public static class Gen5ShaderTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DecodeVop3p(
|
||||
uint word,
|
||||
uint extra,
|
||||
out string name,
|
||||
out uint sizeDwords,
|
||||
out string error)
|
||||
{
|
||||
var opcode = (word >> 16) & 0x7F;
|
||||
var src0 = extra & 0x1FF;
|
||||
var src1 = (extra >> 9) & 0x1FF;
|
||||
var src2 = (extra >> 18) & 0x1FF;
|
||||
sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u;
|
||||
error = string.Empty;
|
||||
|
||||
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
||||
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
||||
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
|
||||
// fail loudly at emission rather than being silently mis-emitted.
|
||||
name = opcode switch
|
||||
{
|
||||
0x0E => "VPkFmaF16",
|
||||
0x0F => "VPkAddF16",
|
||||
0x10 => "VPkMulF16",
|
||||
0x11 => "VPkMinF16",
|
||||
0x12 => "VPkMaxF16",
|
||||
_ => $"Vop3pRaw{opcode:X2}",
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DecodeDs(
|
||||
uint word,
|
||||
out string name,
|
||||
@@ -1886,28 +1839,6 @@ public static class Gen5ShaderTranslator
|
||||
isVop3B ? (word >> 8) & 0x7F : null);
|
||||
break;
|
||||
}
|
||||
case Gen5ShaderEncoding.Vop3p:
|
||||
{
|
||||
var extra = words[1];
|
||||
sources =
|
||||
[
|
||||
Gen5Operand.Source(extra & 0x1FF, literal),
|
||||
Gen5Operand.Source((extra >> 9) & 0x1FF, literal),
|
||||
Gen5Operand.Source((extra >> 18) & 0x1FF, literal),
|
||||
];
|
||||
destinations = [Gen5Operand.Vector(word & 0xFF)];
|
||||
|
||||
// op_sel_hi is split across both dwords: bits [1:0] live in word1
|
||||
// [28:27], bit [2] in word0 [14].
|
||||
var opSelHi = ((extra >> 27) & 0x3) | (((word >> 14) & 0x1) << 2);
|
||||
control = new Gen5Vop3pControl(
|
||||
(word >> 11) & 0x7,
|
||||
opSelHi,
|
||||
(extra >> 29) & 0x7,
|
||||
(word >> 8) & 0x7,
|
||||
((word >> 15) & 1) != 0);
|
||||
break;
|
||||
}
|
||||
case Gen5ShaderEncoding.Ds:
|
||||
{
|
||||
var extra = words[1];
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// 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 Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class Gen5ShaderDecoderBoundaryTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint Export = 0xF8000000;
|
||||
private const uint Nop = 0xBF800000;
|
||||
private const int MaximumInstructionCount = 4096;
|
||||
|
||||
[Fact]
|
||||
public void MissingAddress_IsRejectedWithoutReadingGuestMemory()
|
||||
{
|
||||
var memory = new RecordingCpuMemory(ShaderAddress, []);
|
||||
|
||||
var decoded = Decode(memory, 0, out var program, out var error);
|
||||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("missing", error);
|
||||
Assert.Empty(memory.Reads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreadableFirstWord_IsRejectedAtProgramStart()
|
||||
{
|
||||
var memory = new RecordingCpuMemory(ShaderAddress, []);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("read-failed pc=0x0", error);
|
||||
Assert.Collection(
|
||||
memory.Reads,
|
||||
read => Assert.Equal(new MemoryRead(ShaderAddress, sizeof(uint), false), read));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TruncatedTwoDwordInstruction_IsRejectedAtMissingWord()
|
||||
{
|
||||
var memory = RecordingCpuMemory.FromWords(ShaderAddress, Export);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("read-failed pc=0x4", error);
|
||||
Assert.Collection(
|
||||
memory.Reads,
|
||||
read => Assert.Equal(new MemoryRead(ShaderAddress, sizeof(uint), true), read),
|
||||
read => Assert.Equal(
|
||||
new MemoryRead(ShaderAddress + sizeof(uint), sizeof(uint), false),
|
||||
read));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownTopLevelEncoding_IsRejectedWithoutSpeculativeReads()
|
||||
{
|
||||
const uint unknownTopLevel = 0xE4000000;
|
||||
var memory = RecordingCpuMemory.FromWords(ShaderAddress, unknownTopLevel);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("unknown-top pc=0x0 word=0xE4000000", error);
|
||||
Assert.Collection(
|
||||
memory.Reads,
|
||||
read => Assert.Equal(new MemoryRead(ShaderAddress, sizeof(uint), true), read));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaximumInstructionCountWithoutEndPgm_IsRejectedAsUnterminated()
|
||||
{
|
||||
var words = new uint[MaximumInstructionCount];
|
||||
Array.Fill(words, Nop);
|
||||
var memory = RecordingCpuMemory.FromWords(ShaderAddress, words);
|
||||
|
||||
var decoded = Decode(memory, ShaderAddress, out var program, out var error);
|
||||
|
||||
Assert.False(decoded);
|
||||
Assert.Empty(program.Instructions);
|
||||
Assert.Equal("unterminated", error);
|
||||
Assert.Equal(MaximumInstructionCount, memory.Reads.Count);
|
||||
Assert.All(memory.Reads, read => Assert.True(read.Succeeded));
|
||||
Assert.Equal(
|
||||
new MemoryRead(
|
||||
ShaderAddress + ((MaximumInstructionCount - 1) * sizeof(uint)),
|
||||
sizeof(uint),
|
||||
true),
|
||||
memory.Reads[^1]);
|
||||
}
|
||||
|
||||
private static bool Decode(
|
||||
RecordingCpuMemory memory,
|
||||
ulong address,
|
||||
out Gen5ShaderProgram program,
|
||||
out string error) =>
|
||||
Gen5ShaderTranslator.TryDecodeProgram(
|
||||
new CpuContext(memory, Generation.Gen5),
|
||||
address,
|
||||
out program,
|
||||
out error);
|
||||
|
||||
private readonly record struct MemoryRead(ulong Address, int Length, bool Succeeded);
|
||||
|
||||
private sealed class RecordingCpuMemory(ulong baseAddress, byte[] storage) : ICpuMemory
|
||||
{
|
||||
public List<MemoryRead> Reads { get; } = [];
|
||||
|
||||
public static RecordingCpuMemory FromWords(ulong baseAddress, params uint[] words)
|
||||
{
|
||||
var storage = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
storage.AsSpan(index * sizeof(uint), sizeof(uint)),
|
||||
words[index]);
|
||||
}
|
||||
|
||||
return new RecordingCpuMemory(baseAddress, storage);
|
||||
}
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
var succeeded = TryResolve(virtualAddress, destination.Length, out var offset);
|
||||
Reads.Add(new MemoryRead(virtualAddress, destination.Length, succeeded));
|
||||
if (succeeded)
|
||||
{
|
||||
storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||
|
||||
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (virtualAddress < baseAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relative = virtualAddress - baseAddress;
|
||||
if (relative > (ulong)storage.Length ||
|
||||
(ulong)length > (ulong)storage.Length - relative)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)relative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Loader;
|
||||
|
||||
public sealed class SelfLoaderTests
|
||||
{
|
||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
||||
private const int SelfHeaderSize = 0x20;
|
||||
private const int ElfHeaderSize = 0x40;
|
||||
|
||||
[Theory]
|
||||
[InlineData(Ps4SelfMagic, (byte)0x00, 0x0000_0101u, (ushort)0x22)]
|
||||
[InlineData(Ps5SelfMagic, (byte)0x00, 0x0000_0101u, (ushort)0x22)]
|
||||
[InlineData(Ps5SelfMagic, (byte)0x10, 0x1000_0101u, (ushort)0x32)]
|
||||
public void Load_AcceptsSupportedSelfHeaderVariants(
|
||||
uint magic,
|
||||
byte version,
|
||||
uint keyType,
|
||||
ushort flags)
|
||||
{
|
||||
var imageData = CreateSelfImage(magic, version, keyType, flags);
|
||||
|
||||
var image = new SelfLoader().Load(imageData, new VirtualMemory());
|
||||
|
||||
Assert.True(image.IsSelf);
|
||||
Assert.Equal(2, image.ElfHeader.AbiVersion);
|
||||
Assert.Empty(image.ProgramHeaders);
|
||||
Assert.Empty(image.MappedRegions);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x05, (byte)0x00)]
|
||||
[InlineData(0x06, (byte)0x02)]
|
||||
[InlineData(0x07, (byte)0x00)]
|
||||
public void Load_RejectsUnsupportedStructuralSelfHeaderValues(int offset, byte value)
|
||||
{
|
||||
var imageData = CreateSelfImage(Ps5SelfMagic, 0x10, 0x1000_0101, 0x32);
|
||||
imageData[offset] = value;
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
new SelfLoader().Load(imageData, new VirtualMemory()));
|
||||
}
|
||||
|
||||
private static byte[] CreateSelfImage(uint magic, byte version, uint keyType, ushort flags)
|
||||
{
|
||||
var imageData = new byte[SelfHeaderSize + ElfHeaderSize];
|
||||
var selfHeader = imageData.AsSpan(0, SelfHeaderSize);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(selfHeader, magic);
|
||||
selfHeader[0x04] = version;
|
||||
selfHeader[0x05] = 0x01;
|
||||
selfHeader[0x06] = 0x01;
|
||||
selfHeader[0x07] = 0x12;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(selfHeader[0x08..], keyType);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(selfHeader[0x0C..], SelfHeaderSize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(selfHeader[0x10..], (ulong)imageData.Length);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(selfHeader[0x18..], 0);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(selfHeader[0x1A..], flags);
|
||||
|
||||
WriteMinimalElfHeader(imageData.AsSpan(SelfHeaderSize, ElfHeaderSize));
|
||||
return imageData;
|
||||
}
|
||||
|
||||
private static void WriteMinimalElfHeader(Span<byte> header)
|
||||
{
|
||||
header.Clear();
|
||||
header[0x00] = 0x7F;
|
||||
header[0x01] = (byte)'E';
|
||||
header[0x02] = (byte)'L';
|
||||
header[0x03] = (byte)'F';
|
||||
header[0x04] = 2;
|
||||
header[0x05] = 1;
|
||||
header[0x06] = 1;
|
||||
header[0x07] = 9;
|
||||
header[0x08] = 2;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x10..], 3);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x12..], 62);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x14..], 1);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[0x20..], ElfHeaderSize);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x34..], ElfHeaderSize);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x36..], 0x38);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Silk.NET.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanPhysicalDeviceScoringTests
|
||||
{
|
||||
private const uint NvidiaVendorId = 0x10DE;
|
||||
private const uint AmdVendorId = 0x1002;
|
||||
private const uint IntelVendorId = 0x8086;
|
||||
private const uint QualcommVendorId = 0x5143;
|
||||
|
||||
private static PhysicalDeviceProperties Device(PhysicalDeviceType type, uint vendorId = 0)
|
||||
=> new() { DeviceType = type, VendorID = vendorId };
|
||||
|
||||
private static int Score(PhysicalDeviceType type, uint vendorId = 0)
|
||||
=> VulkanVideoPresenter.ScorePhysicalDevice(Device(type, vendorId), name: string.Empty, deviceOverride: null);
|
||||
|
||||
[Fact]
|
||||
public void RealIntegratedGpuOutranksSoftwareRasterizer()
|
||||
{
|
||||
Assert.True(Score(PhysicalDeviceType.IntegratedGpu) > Score(PhysicalDeviceType.Cpu));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IntelVendorId)]
|
||||
[InlineData(QualcommVendorId)]
|
||||
[InlineData(0u)]
|
||||
public void NonAmdIntegratedGpuBeatsSoftware(uint vendorId)
|
||||
{
|
||||
Assert.True(Score(PhysicalDeviceType.IntegratedGpu, vendorId) > Score(PhysicalDeviceType.Cpu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscreteGpuOutranksIntegratedGpu()
|
||||
{
|
||||
Assert.True(Score(PhysicalDeviceType.DiscreteGpu) > Score(PhysicalDeviceType.IntegratedGpu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NvidiaDiscreteGpuScoresHighest()
|
||||
{
|
||||
var nvidia = Score(PhysicalDeviceType.DiscreteGpu, NvidiaVendorId);
|
||||
Assert.True(nvidia > Score(PhysicalDeviceType.DiscreteGpu));
|
||||
Assert.True(nvidia > Score(PhysicalDeviceType.IntegratedGpu));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeviceOverridePinsMatchingAdapterAndExcludesOthers()
|
||||
{
|
||||
var properties = Device(PhysicalDeviceType.Cpu);
|
||||
Assert.Equal(1000, VulkanVideoPresenter.ScorePhysicalDevice(properties, "Radeon Graphics", "radeon"));
|
||||
Assert.Equal(-1000, VulkanVideoPresenter.ScorePhysicalDevice(properties, "Radeon Graphics", "nvidia"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmdIntegratedGpuIsLastResortOnWindows()
|
||||
{
|
||||
var penalty = VulkanVideoPresenter.ComputeDevicePenalty(
|
||||
Device(PhysicalDeviceType.IntegratedGpu, AmdVendorId), isWindows: true);
|
||||
Assert.True(penalty > 0);
|
||||
Assert.True(50 - penalty < 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmdApuIsNotPenalizedOffWindows()
|
||||
{
|
||||
var penalty = VulkanVideoPresenter.ComputeDevicePenalty(
|
||||
Device(PhysicalDeviceType.IntegratedGpu, AmdVendorId), isWindows: false);
|
||||
Assert.Equal(0, penalty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void AmdDiscreteGpuIsNeverPenalized(bool isWindows)
|
||||
{
|
||||
Assert.Equal(0, VulkanVideoPresenter.ComputeDevicePenalty(
|
||||
Device(PhysicalDeviceType.DiscreteGpu, AmdVendorId), isWindows));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IntelVendorId)]
|
||||
[InlineData(QualcommVendorId)]
|
||||
public void NonAmdIntegratedGpuIsNeverPenalized(uint vendorId)
|
||||
{
|
||||
Assert.Equal(0, VulkanVideoPresenter.ComputeDevicePenalty(
|
||||
Device(PhysicalDeviceType.IntegratedGpu, vendorId), isWindows: true));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user