Compare commits

..

11 Commits

Author SHA1 Message Date
ParantezTech 9ab1e2f21a reuse 2026-07-04 19:14:44 +03:00
ParantezTech 340eb9e0ab [dotnet] configure local nuget package cache 2026-07-04 19:12:18 +03:00
Berk 03e3d25e85 [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix) (#19)
* [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix)

* [reuse] I forgot to add comma

* [readme] update logo
2026-07-04 17:05:09 +03:00
Berk 2bc466b10e [gpu-hotfix1] Cache improvements have been made (6x performance gain) (#18) 2026-07-04 15:19:32 +03:00
Berk 45d0be474b Savedata fix 1 (#17)
* [scePad] Just format code

* [sceSaveData] user folder sometimes appeared in the previous folder has been fixed.
2026-07-04 15:18:21 +03:00
ParantezTech 5bb91eff9b [scePad] Just format code 2026-07-04 15:10:48 +03:00
Vlad Denisov f4df0ed4bd [padExports]: add basic gamepad mappings (#16) 2026-07-04 14:40:43 +03:00
ParantezTech 23691a2bdc [readme] update screenshot image for dreaming sarah 2026-07-04 14:18:51 +03:00
ParantezTech 5c5ed8c064 [readme] update screenshot and add new image for dreaming sarah after shader decoder updates 2026-07-04 14:03:30 +03:00
Berk a4748ce266 [shader-decoder-part1] Implemented a shader decoder (Part 1) (#12)
* [shader-decoder-part1] Implemented a shader decoder for Gen5 shaders, including IR generation, metadata reading, scalar evaluation, and SPIR-V translation. Updated related exports and video output components to support the new shader decoding functionality.

* [shader decoder] correct RDNA2 operands, fixing synchronization problems

* [shader-decoder] RDNA2 decoder improvements

* [shader-decoder] fix RDNA2 shift masking and sprite draws

* [shader-decoder] improve RDNA2 shader decoder to support more instructions and fix some issues with the previous implementation.
2026-07-04 13:51:08 +03:00
Berk 5617646c8a [ci] fix workflow dispatch and push triggers for main branch (#15) 2026-07-04 13:48:29 +03:00
22 changed files with 1703 additions and 293 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ indent_size = 4
indent_style = space
tab_width = 4
[*.{md,json,yml,props,csproj}]
[*.{md,json,yml,xml,props,csproj}]
indent_size = 2
tab_width = 2
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

+1 -1
View File
@@ -126,7 +126,7 @@ jobs:
needs:
- init
- build
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: write
+1
View File
@@ -26,6 +26,7 @@ arm64/
*.userosscache
*.sln.docstates
.packages
packages/
*.nupkg
.nuget/
BIN
View File
Binary file not shown.
+3 -1
View File
@@ -3,12 +3,14 @@ version = 1
[[annotations]]
path = [
"REUSE.toml",
"nuget.config",
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"_logs/**",
".github/images/**"
".github/images/**",
"assets/images/**"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="globalPackagesFolder" value=".packages" />
</config>
<packageSources>
<clear />
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="Nuget">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
+6 -1
View File
@@ -16,7 +16,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> -->
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -30,6 +30,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
+102 -71
View File
@@ -81,7 +81,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -210,7 +210,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -473,30 +473,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
{
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
{
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
{
return true;
}
return true;
}
if (region.IsReservedOnly)
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
}
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
requiresExclusiveAccess = true;
break;
return false;
}
}
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
@@ -533,30 +537,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
{
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
return true;
}
return true;
}
if (region.IsReservedOnly)
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
}
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
requiresExclusiveAccess = true;
break;
return false;
}
}
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
@@ -589,13 +597,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
{
continue;
}
var srcPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
@@ -637,13 +646,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
{
continue;
}
var destPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
@@ -699,15 +709,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
{
if (virtualAddress >= region.VirtualAddress &&
virtualAddress < region.VirtualAddress + region.Size)
{
return (void*)virtualAddress;
}
}
return null;
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
}
finally
{
@@ -720,14 +724,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
{
return true;
}
}
return false;
return FindRegion(virtualAddress, size) is not null;
}
finally
{
@@ -737,14 +734,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private MemoryRegion? FindRegion(ulong address, ulong size)
{
foreach (var region in _regions)
var low = 0;
var high = _regions.Count - 1;
MemoryRegion? candidate = null;
while (low <= high)
{
if (TryResolveRegionOffset(address, size, region, out _))
var middle = low + ((high - low) >> 1);
var region = _regions[middle];
if (region.VirtualAddress <= address)
{
return region;
candidate = region;
low = middle + 1;
}
else
{
high = middle - 1;
}
}
return null;
return candidate is not null &&
TryResolveRegionOffset(address, size, candidate, out _)
? candidate
: null;
}
private void InsertRegionSorted(MemoryRegion region)
{
var low = 0;
var high = _regions.Count;
while (low < high)
{
var middle = low + ((high - low) >> 1);
if (_regions[middle].VirtualAddress < region.VirtualAddress)
{
low = middle + 1;
}
else
{
high = middle;
}
}
_regions.Insert(low, region);
}
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
+112 -10
View File
@@ -142,7 +142,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedDcbSizes = new();
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
private static readonly HashSet<(ulong Es, ulong Ps, ulong Target)> _tracedShaderDraws = new();
private static readonly HashSet<(ulong Es, ulong Ps, ulong Target, ulong Texture, uint VertexCount)> _tracedShaderDraws = new();
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
private static readonly HashSet<ulong> _tracedComputeShaders = new();
@@ -155,6 +155,16 @@ public static class AgcExports
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
byte[]> _computeSpirvCache = new();
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
private static readonly bool _traceAgc = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
"1",
StringComparison.Ordinal);
private static readonly bool _traceAgcShader =
_traceAgc ||
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
"1",
StringComparison.Ordinal);
private static long _dcbWriteDataTraceCount;
private static long _dcbWaitRegMemTraceCount;
private static long _createShaderTraceCount;
@@ -2788,7 +2798,7 @@ public static class AgcExports
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}");
}
if (vertexCount is not (3 or 4 or 6))
if (vertexCount == 0 || vertexCount > 1_048_576)
{
return;
}
@@ -2872,8 +2882,9 @@ public static class AgcExports
lock (_submitTraceGate)
{
var firstTextureAddress = translatedDraw.Textures.FirstOrDefault()?.Descriptor.Address ?? 0;
if (_tracedShaderDraws.Add(
(exportShaderAddress, pixelShaderAddress, firstTarget.Address)))
(exportShaderAddress, pixelShaderAddress, firstTarget.Address, firstTextureAddress, vertexCount)))
{
TraceTranslatedGuestDraw(
ctx,
@@ -3033,6 +3044,11 @@ public static class AgcExports
return false;
}
TraceAgcShader(
$"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " +
$"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " +
$"decoded={FormatTextureDescriptor(texture)} " +
$"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
textures.Add(
new TranslatedImageBinding(
texture,
@@ -3477,7 +3493,8 @@ public static class AgcExports
',',
draw.VertexInputs.Select(input =>
$"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" +
$":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}"));
$":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}" +
$":fmt{input.DataFormat}/num{input.NumberFormat}"));
var scissor = draw.RenderState.Scissor is { } drawScissor
? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}"
: "full";
@@ -3486,6 +3503,29 @@ public static class AgcExports
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" +
$"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}"
: "full";
var rasterRegisters = new (string Name, uint Offset)[]
{
("screen_tl", PaScScreenScissorTl),
("screen_br", PaScScreenScissorBr),
("window_off", PaScWindowOffset),
("window_tl", PaScWindowScissorTl),
("window_br", PaScWindowScissorBr),
("generic_tl", PaScGenericScissorTl),
("generic_br", PaScGenericScissorBr),
("vport_tl", PaScVportScissor0Tl),
("vport_br", PaScVportScissor0Br),
("mode", PaScModeCntl0),
("xscale", PaClVportXScale),
("xoffset", PaClVportXOffset),
("yscale", PaClVportYScale),
("yoffset", PaClVportYOffset),
};
var raster = string.Join(
',',
rasterRegisters.Select(entry =>
state.CxRegisters.TryGetValue(entry.Offset, out var value)
? $"{entry.Name}=0x{value:X8}"
: $"{entry.Name}=missing"));
var blend = draw.RenderState.Blend;
TraceAgcShader(
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
@@ -3493,6 +3533,7 @@ public static class AgcExports
$"primitive=0x{draw.PrimitiveType:X} " +
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
$"raster=[{raster}] " +
$"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " +
$"targets=[{targets}] textures=[{textures}] " +
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
@@ -3550,6 +3591,8 @@ public static class AgcExports
buffers[index] = new VulkanGuestVertexBuffer(
binding.Location,
binding.ComponentCount,
binding.DataFormat,
binding.NumberFormat,
binding.BaseAddress,
binding.Stride,
binding.OffsetBytes,
@@ -3579,7 +3622,10 @@ public static class AgcExports
}
var sourceWidth = descriptor.TileMode == 0
? Math.Max(descriptor.Width, descriptor.Pitch)
? GetLinearTexturePitch(
Math.Max(descriptor.Width, descriptor.Pitch),
descriptor.Height,
descriptor.Format)
: descriptor.Width;
var sourceByteCount = GetTextureByteCount(
descriptor.Format,
@@ -3593,6 +3639,31 @@ public static class AgcExports
return true;
}
if (!isStorage &&
descriptor.Address != 0 &&
VulkanVideoPresenter.IsGuestImageAvailable(
descriptor.Address,
descriptor.Format,
descriptor.NumberType))
{
texture = new VulkanGuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
descriptor.Format,
descriptor.NumberType,
[],
IsFallback: false,
IsStorage: false,
MipLevels: descriptor.MipLevels,
MipLevel: mipLevel,
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
return true;
}
if (isStorage)
{
var initialPixels = Array.Empty<byte>();
@@ -3617,7 +3688,7 @@ public static class AgcExports
IsStorage: true,
MipLevels: descriptor.MipLevels,
MipLevel: mipLevel,
Pitch: descriptor.Pitch,
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
@@ -3663,7 +3734,7 @@ public static class AgcExports
IsStorage: isStorage,
MipLevels: descriptor.MipLevels,
MipLevel: mipLevel,
Pitch: descriptor.Pitch,
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor));
@@ -4326,6 +4397,28 @@ public static class AgcExports
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
}
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format)
{
var bytesPerTexel = GetTextureBytesPerTexel(format);
if (bytesPerTexel == 0 || height == 0)
{
return pitch;
}
var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel);
var alignedPitch = AlignUp(pitch, pitchAlignment);
var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel);
while ((alignedPitch * height) % sliceAlignment != 0)
{
alignedPitch += pitchAlignment;
}
return checked((uint)alignedPitch);
}
private static ulong AlignUp(ulong value, ulong alignment) =>
(value + alignment - 1) & ~(alignment - 1);
private static void TraceShaderTranslationMiss(
CpuContext ctx,
SubmittedDcbState state,
@@ -4594,6 +4687,10 @@ public static class AgcExports
return false;
}
// GFX10/RDNA2 T# layout: WIDTH is split across word1[31:30] (lo 2 bits)
// and word2[11:0] (hi 12 bits); FORMAT is the combined 9-bit field at
// word1[28:20]. Verified against Kyty's decode of the same game
// descriptors (fmt=56=8_8_8_8_UNORM, extent 1280x720, sw_mode 27).
// GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the
// Demon's Souls descriptors both show that only the low 32 bits are
// part of the guest GPU VA. The upper baseaddr bits carry resource
@@ -5343,7 +5440,7 @@ public static class AgcExports
private static void TraceAgc(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
if (!_traceAgc)
{
return;
}
@@ -5353,8 +5450,7 @@ public static class AgcExports
private static void TraceAgcShader(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal))
if (!_traceAgcShader)
{
return;
}
@@ -5367,6 +5463,12 @@ public static class AgcExports
? "none"
: string.Join(',', values.Select(static value => $"{value:X8}"));
private static string FormatTextureDescriptor(TextureDescriptor descriptor) =>
$"addr=0x{descriptor.Address:X16} {descriptor.Width}x{descriptor.Height} " +
$"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " +
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
private static void DumpSpirv(
string stage,
ulong shaderAddress,
+2
View File
@@ -276,6 +276,8 @@ internal sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
@@ -20,7 +20,9 @@ internal static class Gen5ShaderScalarEvaluator
ulong BaseAddress,
uint Stride,
uint NumRecords,
ulong SizeBytes);
ulong SizeBytes,
uint NumberFormat,
uint DataFormat);
public static bool TryResolveImageBindings(
CpuContext ctx,
@@ -400,14 +402,21 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
var bindingData = data;
var bindingStride = descriptor.Stride;
var bindingOffset = unchecked((uint)control.OffsetBytes + scalarOffset);
var bindingDataFormat = descriptor.DataFormat;
var bindingNumberFormat = descriptor.NumberFormat;
binding = new Gen5VertexInputBinding(
instruction.Pc,
location,
control.DwordCount,
bindingDataFormat,
bindingNumberFormat,
descriptor.BaseAddress,
descriptor.Stride,
unchecked((uint)control.OffsetBytes + scalarOffset),
data);
bindingStride,
bindingOffset,
bindingData);
return true;
}
@@ -803,7 +812,11 @@ internal static class Gen5ShaderScalarEvaluator
"SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left),
_ => registers[destination.Value] | (1u << ((int)left & 31)),
};
scalarConditionCode = registers[destination.Value] != 0;
if (instruction.Opcode != "SBitset1B32")
{
scalarConditionCode = registers[destination.Value] != 0;
}
return true;
}
@@ -829,13 +842,15 @@ internal static class Gen5ShaderScalarEvaluator
}
case "SSubU32":
result = left - right;
scalarConditionCode = left >= right;
scalarConditionCode = right > left;
break;
case "SAddI32":
result = unchecked((uint)((int)left + (int)right));
scalarConditionCode = SignedAddOverflow(left, right, result);
break;
case "SSubI32":
result = unchecked((uint)((int)left - (int)right));
scalarConditionCode = SignedSubOverflow(left, right, result);
break;
case "SAddcU32":
{
@@ -846,23 +861,27 @@ internal static class Gen5ShaderScalarEvaluator
}
case "SSubbU32":
{
var borrow = scalarConditionCode ? 0UL : 1UL;
var borrow = scalarConditionCode ? 1UL : 0UL;
var subtrahend = (ulong)right + borrow;
result = unchecked(left - (uint)subtrahend);
scalarConditionCode = left >= subtrahend;
scalarConditionCode = subtrahend > left;
break;
}
case "SMinI32":
result = unchecked((uint)Math.Min((int)left, (int)right));
scalarConditionCode = (int)left < (int)right;
break;
case "SMinU32":
result = Math.Min(left, right);
scalarConditionCode = left < right;
break;
case "SMaxI32":
result = unchecked((uint)Math.Max((int)left, (int)right));
scalarConditionCode = (int)left > (int)right;
break;
case "SMaxU32":
result = Math.Max(left, right);
scalarConditionCode = left > right;
break;
case "SCselectB32":
result = scalarConditionCode ? left : right;
@@ -926,6 +945,7 @@ internal static class Gen5ShaderScalarEvaluator
var offset = (int)right & 31;
var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset);
result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width));
scalarConditionCode = result != 0;
break;
}
case "SBfeI32":
@@ -935,23 +955,41 @@ internal static class Gen5ShaderScalarEvaluator
result = width == 0
? 0
: unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width)));
scalarConditionCode = result != 0;
break;
}
case "SAbsdiffI32":
result = unchecked((uint)Math.Abs((long)(int)left - (int)right));
scalarConditionCode = result != 0;
break;
case "SLshl1AddU32":
result = (left << 1) + right;
break;
{
var wide = ((ulong)left << 1) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break;
}
case "SLshl2AddU32":
result = (left << 2) + right;
break;
{
var wide = ((ulong)left << 2) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break;
}
case "SLshl3AddU32":
result = (left << 3) + right;
break;
{
var wide = ((ulong)left << 3) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break;
}
case "SLshl4AddU32":
result = (left << 4) + right;
break;
{
var wide = ((ulong)left << 4) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break;
}
case "SPackLlB32B16":
result = (left & 0xFFFFu) | (right << 16);
break;
@@ -993,7 +1031,8 @@ internal static class Gen5ShaderScalarEvaluator
"SNandSaveexecB64" or
"SNorSaveexecB64" or
"SXnorSaveexecB64" or
"SAndn1SaveexecB64"))
"SAndn1SaveexecB64" or
"SOrn1SaveexecB64"))
{
return false;
}
@@ -1021,11 +1060,12 @@ internal static class Gen5ShaderScalarEvaluator
"SAndSaveexecB64" => oldExec & source,
"SOrSaveexecB64" => oldExec | source,
"SXorSaveexecB64" => oldExec ^ source,
"SAndn1SaveexecB64" => ~oldExec & source,
"SAndn2SaveexecB64" => oldExec & ~source,
"SOrn2SaveexecB64" => oldExec | ~source,
"SNandSaveexecB64" => ~(oldExec & source),
"SNorSaveexecB64" => ~(oldExec | source),
"SAndn1SaveexecB64" => ~source & oldExec,
"SAndn2SaveexecB64" => source & ~oldExec,
"SOrn1SaveexecB64" => ~source | oldExec,
"SOrn2SaveexecB64" => source | ~oldExec,
"SNandSaveexecB64" => ~(source & oldExec),
"SNorSaveexecB64" => ~(source | oldExec),
_ => ~(oldExec ^ source),
};
@@ -1095,6 +1135,12 @@ internal static class Gen5ShaderScalarEvaluator
private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask;
private static bool SignedAddOverflow(uint left, uint right, uint result) =>
((left ^ result) & (right ^ result) & 0x80000000u) != 0;
private static bool SignedSubOverflow(uint left, uint right, uint result) =>
((left ^ right) & (left ^ result) & 0x80000000u) != 0;
private static bool TryExecuteScalarCompare(
Gen5ShaderInstruction instruction,
uint[] registers,
@@ -1416,7 +1462,7 @@ internal static class Gen5ShaderScalarEvaluator
word2 == 0 &&
word3 == 0)
{
descriptor = new BufferDescriptor(0, 0, 0, 0);
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
return true;
}
@@ -1428,19 +1474,64 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
descriptor = new BufferDescriptor(0, 0, 0, 0);
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
return true;
}
var baseAddress = word0 | ((ulong)(word1 & 0x0FFFu) << 32);
var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32);
var stride = (word1 >> 16) & 0x3FFFu;
var unifiedFormat = (word3 >> 12) & 0x7Fu;
var (dataFormat, numberFormat) =
DecodeGfx10BufferFormat(unifiedFormat);
var sizeBytes = stride == 0
? word2
: (ulong)stride * word2;
descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes);
descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes, numberFormat, dataFormat);
return true;
}
private static (uint DataFormat, uint NumberFormat)
DecodeGfx10BufferFormat(uint format) =>
format switch
{
0 => (0, 0),
>= 1 and <= 6 => (1, format - 1),
>= 7 and <= 13 => (2, DecodeUnifiedNumber(format - 7, 7)),
>= 14 and <= 19 => (3, format - 14),
>= 20 and <= 22 => (4, DecodeIntegerOrFloatNumber(format - 20)),
>= 23 and <= 29 => (5, DecodeUnifiedNumber(format - 23, 7)),
>= 30 and <= 36 => (6, DecodeUnifiedNumber(format - 30, 7)),
>= 37 and <= 43 => (7, DecodeUnifiedNumber(format - 37, 7)),
>= 44 and <= 49 => (8, format - 44),
>= 50 and <= 55 => (9, format - 50),
>= 56 and <= 61 => (10, format - 56),
>= 62 and <= 64 => (11, DecodeIntegerOrFloatNumber(format - 62)),
>= 65 and <= 71 => (12, DecodeUnifiedNumber(format - 65, 7)),
>= 72 and <= 74 => (13, DecodeIntegerOrFloatNumber(format - 72)),
>= 75 and <= 77 => (14, DecodeIntegerOrFloatNumber(format - 75)),
128 => (1, 9),
129 => (3, 9),
130 => (10, 9),
132 => (34, 7),
133 => (16, 0),
134 => (17, 0),
135 => (18, 0),
136 => (19, 0),
140 => (4, 7),
_ => (0, 0),
};
private static uint DecodeUnifiedNumber(uint offset, uint formatCount) =>
offset == formatCount - 1 ? 7u : offset;
private static uint DecodeIntegerOrFloatNumber(uint offset) =>
offset switch
{
0 => 4,
1 => 5,
_ => 7,
};
private static bool TryReadUserDataScalarLoad(
Gen5ShaderState state,
Gen5ShaderInstruction instruction,
+50 -5
View File
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Text;
namespace SharpEmu.Libs.Agc;
@@ -13,6 +14,14 @@ internal static class Gen5ShaderTranslator
private const int MaxInstructions = 4096;
private const int MinimumUserDataDwords = 16;
private const int MaximumUserDataDwords = 256;
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
private sealed class ShaderDecodeCache
{
public object Gate { get; } = new();
public Dictionary<ulong, Gen5ShaderProgram> Programs { get; } = new();
public Dictionary<ulong, Gen5ShaderMetadata?> Metadata { get; } = new();
}
private static readonly uint[] FullscreenBarycentricEs =
[
@@ -117,16 +126,51 @@ internal static class Gen5ShaderTranslator
uint userDataScalarRegisterBase = 0)
{
state = default!;
if (!TryDecodeProgram(ctx, shaderAddress, out var program, out error))
error = string.Empty;
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
Gen5ShaderProgram? program;
lock (cache.Gate)
{
return false;
cache.Programs.TryGetValue(shaderAddress, out program);
}
if (program is null)
{
if (!TryDecodeProgram(ctx, shaderAddress, out program, out error))
{
return false;
}
lock (cache.Gate)
{
cache.Programs.TryAdd(shaderAddress, program);
}
}
Gen5ShaderMetadata? metadata = null;
if (shaderHeaderAddress != 0 &&
Gen5ShaderMetadataReader.TryRead(ctx, shaderHeaderAddress, out var decodedMetadata))
if (shaderHeaderAddress != 0)
{
metadata = decodedMetadata;
var metadataCached = false;
lock (cache.Gate)
{
metadataCached = cache.Metadata.TryGetValue(shaderHeaderAddress, out metadata);
}
if (!metadataCached)
{
if (Gen5ShaderMetadataReader.TryRead(
ctx,
shaderHeaderAddress,
out var decodedMetadata))
{
metadata = decodedMetadata;
}
lock (cache.Gate)
{
cache.Metadata.TryAdd(shaderHeaderAddress, metadata);
}
}
}
var userData = new uint[GetUserDataDwordCount(metadata)];
@@ -452,6 +496,7 @@ internal static class Gen5ShaderTranslator
0x2A => "SNorSaveexecB64",
0x2B => "SXnorSaveexecB64",
0x37 => "SAndn1SaveexecB64",
0x38 => "SOrn1SaveexecB64",
_ => string.Empty,
};
+267 -25
View File
@@ -212,12 +212,26 @@ internal static partial class Gen5SpirvTranslator
case "VSinF32":
result = EmitFloatResult(
instruction,
Ext(13, _floatType, GetFloatSource(instruction, 0)));
Ext(
13,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
Float(MathF.Tau))));
break;
case "VCosF32":
result = EmitFloatResult(
instruction,
Ext(14, _floatType, GetFloatSource(instruction, 0)));
Ext(
14,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
Float(MathF.Tau))));
break;
case "VAddF32":
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
@@ -635,13 +649,29 @@ internal static partial class Gen5SpirvTranslator
width);
break;
}
case "VBfiB32":
{
var mask = GetRawSource(instruction, 0);
var insert = GetRawSource(instruction, 1);
var source = GetRawSource(instruction, 2);
result = _module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
BitwiseAnd(mask, insert),
BitwiseAnd(
_module.AddInstruction(SpirvOp.Not, _uintType, mask),
source));
break;
}
case "VCvtPkrtzF16F32":
{
var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0));
var second = TruncateFloat32ForPack(GetFloatSource(instruction, 1));
var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct,
_vec2Type,
GetFloatSource(instruction, 0),
GetFloatSource(instruction, 1));
first,
second);
result = Ext(58, _uintType, vector);
break;
}
@@ -945,6 +975,16 @@ internal static partial class Gen5SpirvTranslator
StoreS(destination, result);
Store(_scc, IsNotZero(result));
return true;
case "SBitset1B32":
result = _module.AddInstruction(
SpirvOp.BitFieldInsert,
_uintType,
LoadS(destination),
UInt(1),
BitwiseAnd(left, UInt(31)),
UInt(1));
StoreS(destination, result);
return true;
default:
{
if (instruction.Sources.Count < 2)
@@ -971,13 +1011,14 @@ internal static partial class Gen5SpirvTranslator
left,
right);
Store(_scc, _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
SpirvOp.UGreaterThan,
_boolType,
left,
right));
right,
left));
break;
case "SAddI32":
result = IAdd(left, right);
Store(_scc, SignedAddOverflow(left, right, result));
break;
case "SSubI32":
result = _module.AddInstruction(
@@ -985,6 +1026,7 @@ internal static partial class Gen5SpirvTranslator
_uintType,
left,
right);
Store(_scc, SignedSubOverflow(left, right, result));
break;
case "SAddcU32":
{
@@ -1021,8 +1063,8 @@ internal static partial class Gen5SpirvTranslator
SpirvOp.Select,
_uintType,
Load(_boolType, _scc),
UInt(0),
UInt(1));
UInt(1),
UInt(0));
var partial = _module.AddInstruction(
SpirvOp.ISub,
_uintType,
@@ -1033,23 +1075,31 @@ internal static partial class Gen5SpirvTranslator
_uintType,
partial,
borrow);
var firstNoBorrow = _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
var firstBorrow = _module.AddInstruction(
SpirvOp.UGreaterThan,
_boolType,
left,
right);
var secondNoBorrow = _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
right,
left);
var secondBorrow = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
partial,
borrow);
_module.AddInstruction(
SpirvOp.IEqual,
_boolType,
borrow,
UInt(1)),
_module.AddInstruction(
SpirvOp.IEqual,
_boolType,
right,
left));
Store(
_scc,
_module.AddInstruction(
SpirvOp.LogicalAnd,
SpirvOp.LogicalOr,
_boolType,
firstNoBorrow,
secondNoBorrow));
firstBorrow,
secondBorrow));
break;
}
case "SMulI32":
@@ -1061,6 +1111,7 @@ internal static partial class Gen5SpirvTranslator
break;
case "SAndB32":
result = BitwiseAnd(left, right);
Store(_scc, IsNotZero(result));
break;
case "SOrB32":
result = _module.AddInstruction(
@@ -1068,6 +1119,7 @@ internal static partial class Gen5SpirvTranslator
_uintType,
left,
right);
Store(_scc, IsNotZero(result));
break;
case "SXorB32":
result = _module.AddInstruction(
@@ -1075,22 +1127,64 @@ internal static partial class Gen5SpirvTranslator
_uintType,
left,
right);
Store(_scc, IsNotZero(result));
break;
case "SAndn2B32":
result = BitwiseAnd(
left,
_module.AddInstruction(SpirvOp.Not, _uintType, right));
Store(_scc, IsNotZero(result));
break;
case "SOrn2B32":
result = _module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
left,
_module.AddInstruction(SpirvOp.Not, _uintType, right));
Store(_scc, IsNotZero(result));
break;
case "SNandB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
BitwiseAnd(left, right));
Store(_scc, IsNotZero(result));
break;
case "SNorB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
_module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
left,
right));
Store(_scc, IsNotZero(result));
break;
case "SXnorB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
_module.AddInstruction(
SpirvOp.BitwiseXor,
_uintType,
left,
right));
Store(_scc, IsNotZero(result));
break;
case "SLshlB32":
result = ShiftLeftLogical(left, right);
Store(_scc, IsNotZero(result));
break;
case "SLshrB32":
result = ShiftRightLogical(
left,
BitwiseAnd(right, UInt(31)));
Store(_scc, IsNotZero(result));
break;
case "SAshrI32":
result = ShiftRightArithmetic(left, right);
Store(_scc, IsNotZero(result));
break;
case "SBfmB32":
result = _module.AddInstruction(
@@ -1133,6 +1227,7 @@ internal static partial class Gen5SpirvTranslator
left,
offset,
width);
Store(_scc, IsNotZero(result));
break;
}
case "SCselectB32":
@@ -1145,9 +1240,47 @@ internal static partial class Gen5SpirvTranslator
break;
case "SMinU32":
result = Ext(38, _uintType, left, right);
Store(
_scc,
_module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
left,
right));
break;
case "SMinI32":
result = Bitcast(
_uintType,
Ext(39, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
Store(
_scc,
_module.AddInstruction(
SpirvOp.SLessThan,
_boolType,
Bitcast(_intType, left),
Bitcast(_intType, right)));
break;
case "SMaxU32":
result = Ext(41, _uintType, left, right);
Store(
_scc,
_module.AddInstruction(
SpirvOp.UGreaterThan,
_boolType,
left,
right));
break;
case "SMaxI32":
result = Bitcast(
_uintType,
Ext(42, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
Store(
_scc,
_module.AddInstruction(
SpirvOp.SGreaterThan,
_boolType,
Bitcast(_intType, left),
Bitcast(_intType, right)));
break;
case "SLshl1AddU32":
case "SLshl2AddU32":
@@ -1292,17 +1425,55 @@ internal static partial class Gen5SpirvTranslator
"SXorSaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseXor, _ulongType, oldExec, left),
"SAndn2SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd, _ulongType, oldExec, notLeft),
"SAndn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd,
_ulongType,
left,
_module.AddInstruction(
SpirvOp.Not,
_ulongType,
oldExec),
left),
oldExec)),
"SAndn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd,
_ulongType,
notLeft,
oldExec),
"SOrn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseOr,
_ulongType,
notLeft,
oldExec),
"SOrn2SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseOr, _ulongType, oldExec, notLeft),
SpirvOp.BitwiseOr,
_ulongType,
left,
_module.AddInstruction(
SpirvOp.Not,
_ulongType,
oldExec)),
"SNandSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseAnd,
_ulongType,
left,
oldExec)),
"SNorSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseOr,
_ulongType,
left,
oldExec)),
"SXnorSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseXor,
_ulongType,
left,
oldExec)),
_ => 0u,
};
if (newExec == 0)
@@ -1508,6 +1679,22 @@ internal static partial class Gen5SpirvTranslator
}
StoreS64(destination, value);
if (instruction.Opcode is
"SNotB64" or
"SAndB64" or
"SOrB64" or
"SXorB64" or
"SAndn1B64" or
"SAndn2B64" or
"SOrn1B64" or
"SOrn2B64" or
"SNandB64" or
"SNorB64" or
"SXnorB64")
{
Store(_scc, IsNotZero64(value));
}
return true;
}
@@ -2067,6 +2254,14 @@ internal static partial class Gen5SpirvTranslator
return Bitcast(_uintType, value);
}
private uint TruncateFloat32ForPack(uint value)
{
var raw = BitwiseAnd(
Bitcast(_uintType, value),
UInt(0xFFFF_E000));
return Bitcast(_floatType, raw);
}
private uint Ext(uint operation, uint resultType, params uint[] operands)
{
var values = new uint[2 + operands.Length];
@@ -2086,6 +2281,53 @@ internal static partial class Gen5SpirvTranslator
value,
_module.Constant64(_ulongType, 0));
private uint SignBit(uint value) =>
ShiftRightLogical(value, UInt(31));
private uint SignedAddOverflow(uint left, uint right, uint result)
{
var leftSign = SignBit(left);
var rightSign = SignBit(right);
var resultSign = SignBit(result);
var sameSourceSign = _module.AddInstruction(
SpirvOp.IEqual,
_boolType,
leftSign,
rightSign);
var resultSignChanged = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
resultSign);
return _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
sameSourceSign,
resultSignChanged);
}
private uint SignedSubOverflow(uint left, uint right, uint result)
{
var leftSign = SignBit(left);
var rightSign = SignBit(right);
var resultSign = SignBit(result);
var differentSourceSign = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
rightSign);
var resultSignChanged = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
resultSign);
return _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
differentSourceSign,
resultSignChanged);
}
private static bool TryDecodeInlineConstant(uint encoded, out uint value)
{
if (encoded == 125)
+69 -4
View File
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
@@ -58,6 +59,7 @@ public static class PadExports
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
}
Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options");
return SetReturn(ctx, PrimaryPadHandle);
}
@@ -162,10 +164,19 @@ public static class PadExports
{
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
data[0x04] = 128;
data[0x05] = 128;
data[0x06] = 128;
data[0x07] = 128;
var acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
data[0x04] = leftX;
data[0x05] = leftY;
data[0x06] = rightX;
data[0x07] = rightY;
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
@@ -185,4 +196,58 @@ public static class PadExports
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
private static bool IsKeyDown(int vk) =>
(GetAsyncKeyState(vk) & 0x8000) != 0;
private static bool IsEmulatorWindowFocused()
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == 0)
{
return false;
}
GetWindowThreadProcessId(foregroundWindow, out var processId);
return processId == (uint)Environment.ProcessId;
}
private static uint ReadKeyboardButtons()
{
uint buttons = 0;
// D-pad
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
// Face buttons
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
// Shoulder buttons
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
// Options (Start)
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
return buttons;
}
private static byte ReadAnalogStick(bool negative, bool positive)
{
if (negative && !positive) return 0;
if (positive && !negative) return 255;
return 128;
}
}
@@ -400,7 +400,7 @@ public static class SaveDataExports
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
var root = string.IsNullOrWhiteSpace(configured)
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
: configured;
return Path.GetFullPath(root);
}
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -48,6 +49,13 @@ public static class VideoOutExports
private static int _frameDumpCount;
private static long _nextFrameDumpIndex;
private static string _windowTitle = "SharpEmu VideoOut";
private static readonly bool _logFrameRate = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
"1",
StringComparison.Ordinal);
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
private static long _submittedFrameCount;
private static long _presentedFrameCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
{
@@ -812,9 +820,46 @@ public static class VideoOutExports
}
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
ReportFrameRate(presented: false);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
internal static void ReportPresentedFrame() =>
ReportFrameRate(presented: true);
private static void ReportFrameRate(bool presented)
{
if (!_logFrameRate)
{
return;
}
if (presented)
{
Interlocked.Increment(ref _presentedFrameCount);
}
else
{
Interlocked.Increment(ref _submittedFrameCount);
}
var started = Volatile.Read(ref _frameRateWindowStart);
var now = Stopwatch.GetTimestamp();
var elapsedTicks = now - started;
if (elapsedTicks < Stopwatch.Frequency ||
Interlocked.CompareExchange(ref _frameRateWindowStart, now, started) != started)
{
return;
}
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
Console.Error.WriteLine(
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
$"presented_fps={presentedCount / elapsedSeconds:F1}");
}
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
{
lock (_stateGate)
File diff suppressed because it is too large Load Diff