mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
30fdd8d6ed
* [ShaderCompiler] Extract the backend-neutral shader compiler project Move the Gen5 (gfx10) microcode decoder, the scalar evaluator, the shader IR, and the metadata reader out of SharpEmu.Libs/Agc into a new SharpEmu.ShaderCompiler project — the half of shader compilation every codegen backend (SPIR-V today; MSL and DXIL later) consumes. Types go public: they are the contract now. Nothing in the project may depend on a host graphics API; the SPIR-V-specific artifact types (Gen5SpirvShader, Gen5SpirvStage) stay beside the emitter in Libs. Three couplings surfaced by the move, each resolved at the right depth: GuestDrawKind was defined inside VulkanVideoPresenter despite being a guest-domain, decoder-produced concept — it moves to the shared project; the evaluator's one HLE dependency (the tracked-libc-heap read fallback) becomes an injectable hook that a Libs module initializer installs before any caller can reach the evaluator; and the inline- constant table is promoted to a shared Gen5InlineConstants so backends cannot drift on constant semantics (the SPIR-V translator now delegates to it). The ShaderDump tool drops its reflection over the moved types in favor of direct typed calls; only the SPIR-V emitter, still internal to Libs until it moves to its own backend project, is reached via reflection. Verified by a clean solution build, the existing test suite, and a full ShaderDump conformance run. * [ShaderCompiler] Move the SPIR-V emitter into SharpEmu.ShaderCompiler.Vulkan Gen5SpirvTranslator (with its ALU partial), SpirvModuleBuilder, SpirvFixedShaders, and the Gen5SpirvShader/Gen5SpirvStage artifact types move whole from SharpEmu.Libs/Agc into the first per-backend codegen project. Notably it needs no Vulkan bindings reference: emitters produce bytes from the shared IR; renderers own graphics APIs. Types go public as the backend's contract; AgcExports and the presenter consume them exactly as before. The ShaderDump tool drops its last reflection: with both halves of the pipeline public it drives decode and all three emit entry points with direct typed calls, retiring the PadWithDefaults invoke shim — and it no longer references SharpEmu.Libs at all, making the conformance tool emulator-independent by design. Verified by a clean solution build, the test suite, a full ShaderDump conformance run, and a locked-mode restore under the pinned SDK. * [Gpu] Extract the guest-GPU backend seam (IGuestGpuBackend) The AGC/VideoOut/SystemService export layers now reach the renderer through IGuestGpuBackend via GuestGpu.Current (mirroring HostPlatform), instead of calling VulkanVideoPresenter statics. The Vulkan backend is a thin adapter over the existing presenter, so the extraction stays mechanical; only the adapter and the presenter itself reference the presenter now. The types crossing the seam move to Gpu/GuestGpuTypes.cs and drop their Vulkan prefixes, which an audit showed were misnomers: every field is a neutral primitive or a raw guest value (guest addresses, format and number-type codes, CB_BLEND register bitfields, verbatim sampler descriptor dwords). The one genuine Vulkan value in the old surface — the Silk.NET Format inside VulkanRenderTargetFormat, which callers never read — stops crossing: TryDecodeRenderTargetFormat is replaced at the seam by TryGetRenderTargetOutputKind, which surfaces only the Gen5PixelOutputKind callers actually consume, keeping native formats a backend-internal concern. ToVulkanSampler in AgcExports is renamed ToGuestSampler to match what it always produced. Seam rules are documented on the interface: no host-API value crosses, and submission stays coarse-grained with synchronization internal to backends. Interim exception, resolved next: shader parameters are still SPIR-V blobs. * [Gpu] Move shader compilation behind the guest-GPU backend The seam's interim exception is gone: AgcExports no longer calls Gen5SpirvTranslator or handles SPIR-V bytes. IGuestGpuBackend gains the three TryCompile entry points, which take the backend-neutral (Gen5ShaderState, Gen5ShaderEvaluation) contract plus the flat per-role resource-slot bases a multi-stage draw needs, and return opaque IGuestCompiledShader handles that only the producing backend can submit — the Vulkan backend wraps its SPIR-V in VulkanCompiledGuestShader and rejects foreign handles loudly. Draw and dispatch submissions take handles instead of byte arrays; the shader caches in AgcExports store handles. IGuestCompiledShader.Payload exposes the backend-defined compiled bytes for exactly two callers: the diagnostics dump and the size trace — documented as never-interpret. The unused _pixelSpirvCache is deleted. With this, a Metal or DX12 backend plugs in by implementing IGuestGpuBackend with its own codegen; nothing in the export layers knows which shader format exists. Verified by a clean solution build, the test suite, and a full ShaderDump conformance run under the pinned SDK. * [Gpu] Fix rename collateral from the seam extraction Address review findings: a doc comment picked up the mechanical VulkanVideoPresenter -> GuestGpu.Current rewrite and ended up naming members that do not exist on the interface, and CreateVulkanIndexBuffer kept its Vulkan prefix while every sibling factory was de-Vulkanized — it produces the neutral GuestIndexBuffer, so it is CreateGuestIndexBuffer. * [Gpu] Label diagnostics dumps with the backend's payload extension Address the review's altitude finding on DumpSpirv: the dump helper's IR-disassembly half is backend-neutral and stays put, but writing the opaque payload to a hardcoded .spv interpreted bytes the seam says never to interpret. IGuestCompiledShader now declares its payload's file extension, and the renamed DumpCompiledShader takes the handle and writes honestly-labeled dumps whichever backend produced them. * [Gpu] Make the shader-cache hit path allocation-free and lock-free Every translated draw built its cache key with a LINQ Select feeding string.Join plus one interpolated string per render target — steady per-draw allocation whether or not the shaders were already cached. The output layout is now packed exactly into a ulong (guest slot in 6 bits + output kind in 2 bits per target, host locations being the byte positions, target count in the key beside it), and the Gen5PixelOutputBinding array is only materialized on a cache miss, where compilation dwarfs it. The graphics/compute shader caches switch from Dictionary guarded by _submitTraceGate to ConcurrentDictionary, making the per-draw and per-dispatch hit paths lock-free and decoupling them from the tracing gate they coincidentally shared. And the seam-shaped render-target list is built once when a translated draw is created instead of a Select/ToArray per submission of a cached draw. * [Gpu] Replace LINQ with explicit loops in code this branch introduced Project rule going forward: no LINQ — it allocates enumerators, closures, and delegates, and this codebase is GC-pause-sensitive. The pixel-output and guest-render-target array builds and the ShaderDump store-PC collection become plain loops; pre-existing LINQ elsewhere is left for changes that already touch those lines. * [ShaderCompiler] Suppress CA2255 on the evaluator hook installer The analyzer coverage that arrived with the rebase flags ModuleInitializer in library code; this is the rule's intended advanced scenario — the hook must be installed before any code path can reach the evaluator, and every such path enters through this assembly — so suppress with that justification rather than weaken the guarantee to a static constructor's lazier timing. * [Gpu] Resolve rebase artifacts onto main Dedupe the System.Collections.Concurrent using in AgcExports that the rebase merge duplicated (main and this branch each added it), and regenerate the lock files for the new shader-compiler projects and SharpEmu.Libs against main's current package graph so --locked-mode restore matches at the branch tip. * [CI] Comment per-platform build artifact links on PRs Adds a workflow_run workflow that, after "Build and Release" finishes a pull-request build, posts (and keeps updated in place) a single PR comment linking the Windows, Linux, and macOS artifacts from that run. It runs via workflow_run rather than in the build workflow because PRs from forks build with a read-only token that cannot comment; the follow-on run executes in the base-repo context with write access and without checking out fork code. GitHub only triggers workflow_run from the default branch, so this takes effect once merged to main.
1842 lines
63 KiB
C#
1842 lines
63 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using System.Numerics;
|
|
|
|
namespace SharpEmu.ShaderCompiler;
|
|
|
|
public static class Gen5ShaderScalarEvaluator
|
|
{
|
|
/// <summary>
|
|
/// Optional fallback for global-memory reads that ctx.Memory cannot satisfy (the
|
|
/// emulator installs the HLE-tracked libc heap reader here at module load). Kept as
|
|
/// a hook so this project never depends on the HLE module implementations.
|
|
/// </summary>
|
|
public static Gen5FallbackMemoryReader? FallbackMemoryReader { get; set; }
|
|
|
|
public delegate bool Gen5FallbackMemoryReader(ulong baseAddress, Span<byte> destination);
|
|
|
|
private const int ScalarRegisterCount = 256;
|
|
private const int ImageDescriptorDwords = 8;
|
|
private const int SamplerDescriptorDwords = 4;
|
|
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
|
private static readonly int DefaultGlobalMemoryBindingBytes =
|
|
int.TryParse(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
|
|
out var configured) && configured >= sizeof(uint)
|
|
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
|
: 1 * 1024 * 1024;
|
|
|
|
public static long GlobalMemoryReadCount;
|
|
public static long GlobalMemoryReadBytes;
|
|
public static long GlobalMemoryReadCacheHits;
|
|
public static long GlobalMemoryReadPvmBytes;
|
|
public static long GlobalMemoryReadLibcBytes;
|
|
public static long GlobalMemoryReadReuses;
|
|
|
|
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
|
private static readonly object _crossFrameReadGate = new();
|
|
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
|
|
private static long _crossFrameReadCacheBytes;
|
|
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
|
|
|
private readonly record struct BufferDescriptor(
|
|
ulong BaseAddress,
|
|
uint Stride,
|
|
uint NumRecords,
|
|
ulong SizeBytes,
|
|
uint NumberFormat,
|
|
uint DataFormat);
|
|
|
|
public static bool TryResolveImageBindings(
|
|
CpuContext ctx,
|
|
Gen5ShaderState state,
|
|
out IReadOnlyList<Gen5ImageBinding> bindings,
|
|
out string error)
|
|
{
|
|
if (TryEvaluate(ctx, state, out var evaluation, out error))
|
|
{
|
|
bindings = evaluation.ImageBindings;
|
|
return true;
|
|
}
|
|
|
|
bindings = [];
|
|
return false;
|
|
}
|
|
|
|
public static bool TryEvaluate(
|
|
CpuContext ctx,
|
|
Gen5ShaderState state,
|
|
out Gen5ShaderEvaluation evaluation,
|
|
out string error,
|
|
bool resolveVertexInputs = false,
|
|
uint? vertexRecordLimit = null)
|
|
{
|
|
evaluation = default!;
|
|
error = string.Empty;
|
|
var scalarRegisters = new uint[ScalarRegisterCount];
|
|
for (var index = 0;
|
|
index < state.UserData.Count &&
|
|
state.UserDataScalarRegisterBase + (uint)index < scalarRegisters.Length;
|
|
index++)
|
|
{
|
|
scalarRegisters[state.UserDataScalarRegisterBase + (uint)index] =
|
|
state.UserData[index];
|
|
}
|
|
|
|
if (state.ComputeSystemRegisters is { } computeSystemRegisters)
|
|
{
|
|
computeSystemRegisters.ClearStaticValues(scalarRegisters);
|
|
}
|
|
|
|
var execMask = RdnaWaveMask;
|
|
WriteScalarPair(scalarRegisters, 106, 0, ref execMask);
|
|
WriteScalarPair(scalarRegisters, 126, execMask, ref execMask);
|
|
var initialScalarRegisters = (uint[])scalarRegisters.Clone();
|
|
|
|
var resolved = new List<Gen5ImageBinding>();
|
|
var globalMemoryBindings = new List<Gen5GlobalMemoryBinding>();
|
|
var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>();
|
|
var vertexInputBindings = new List<Gen5VertexInputBinding>();
|
|
var runtimeScalarRegisters = CollectRuntimeScalarRegisters(state.Program);
|
|
var scalarRegisterSnapshots = new Dictionary<uint, IReadOnlyList<uint>>();
|
|
var scalarConditionCode = false;
|
|
uint? skipUntilPc = null;
|
|
|
|
foreach (var instruction in state.Program.Instructions)
|
|
{
|
|
if (skipUntilPc.HasValue)
|
|
{
|
|
if (instruction.Pc < skipUntilPc.Value)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
skipUntilPc = null;
|
|
}
|
|
|
|
scalarRegisterSnapshots[instruction.Pc] = (uint[])scalarRegisters.Clone();
|
|
|
|
if (instruction.Opcode == "SEndpgm")
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (instruction.Opcode == "SBranch" &&
|
|
TryGetSoppBranchTargetPc(instruction, out var targetPc) &&
|
|
targetPc > instruction.Pc)
|
|
{
|
|
skipUntilPc = targetPc;
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Encoding == Gen5ShaderEncoding.Sopc)
|
|
{
|
|
if (!TryExecuteScalarCompare(
|
|
instruction,
|
|
scalarRegisters,
|
|
out scalarConditionCode,
|
|
out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Encoding == Gen5ShaderEncoding.Sopk &&
|
|
instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal))
|
|
{
|
|
if (!TryExecuteScalarCompareK(
|
|
instruction,
|
|
scalarRegisters,
|
|
out scalarConditionCode,
|
|
out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Encoding is
|
|
Gen5ShaderEncoding.Sop1 or
|
|
Gen5ShaderEncoding.Sop2 or
|
|
Gen5ShaderEncoding.Sopk)
|
|
{
|
|
if (instruction.Opcode is "SSetpcB64" or "SSwappcB64")
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!TryExecuteScalarAlu(
|
|
instruction,
|
|
state.Program.Address,
|
|
scalarRegisters,
|
|
ref execMask,
|
|
ref scalarConditionCode,
|
|
out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Control is Gen5ScalarMemoryControl scalarMemory)
|
|
{
|
|
foreach (var destination in instruction.Destinations)
|
|
{
|
|
if (destination.Kind == Gen5OperandKind.ScalarRegister && destination.Value < ScalarRegisterCount)
|
|
{
|
|
runtimeScalarRegisters.Add(destination.Value);
|
|
}
|
|
}
|
|
|
|
if (!TryExecuteScalarLoad(ctx, state, instruction, scalarMemory, scalarRegisters, globalMemoryBindings, globalMemoryByAddress, runtimeScalarRegisters, out error))
|
|
{
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Control is Gen5GlobalMemoryControl globalMemory)
|
|
{
|
|
if (globalMemory.ScalarAddress >= ScalarRegisterCount - 1)
|
|
{
|
|
error =
|
|
$"global-address-register-range pc=0x{instruction.Pc:X} " +
|
|
$"s{globalMemory.ScalarAddress}";
|
|
return false;
|
|
}
|
|
|
|
var baseAddress =
|
|
scalarRegisters[globalMemory.ScalarAddress] |
|
|
((ulong)scalarRegisters[globalMemory.ScalarAddress + 1] << 32);
|
|
if (baseAddress == 0)
|
|
{
|
|
error = $"global-address-null pc=0x{instruction.Pc:X}";
|
|
return false;
|
|
}
|
|
|
|
var key = (globalMemory.ScalarAddress, baseAddress);
|
|
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
|
{
|
|
if (existingBinding.InstructionPcs is List<uint> instructionPcs)
|
|
{
|
|
instructionPcs.Add(instruction.Pc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
byte[] data;
|
|
if (!TryReadGlobalMemory(ctx, baseAddress, out data))
|
|
{
|
|
error =
|
|
$"global-memory-read-failed pc=0x{instruction.Pc:X} " +
|
|
$"address=0x{baseAddress:X16}";
|
|
return false;
|
|
}
|
|
|
|
var binding = new Gen5GlobalMemoryBinding(
|
|
globalMemory.ScalarAddress,
|
|
baseAddress,
|
|
new List<uint> { instruction.Pc },
|
|
data);
|
|
globalMemoryByAddress.Add(key, binding);
|
|
globalMemoryBindings.Add(binding);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Control is Gen5BufferMemoryControl bufferMemory)
|
|
{
|
|
if (bufferMemory.ScalarResource >= ScalarRegisterCount - 3)
|
|
{
|
|
error =
|
|
$"buffer-resource-register-range pc=0x{instruction.Pc:X} " +
|
|
$"s{bufferMemory.ScalarResource}";
|
|
return false;
|
|
}
|
|
|
|
if (!TryDecodeBufferDescriptor(
|
|
scalarRegisters,
|
|
bufferMemory.ScalarResource,
|
|
strictType: true,
|
|
out var bufferDescriptor))
|
|
{
|
|
error =
|
|
$"buffer-descriptor-invalid pc=0x{instruction.Pc:X} " +
|
|
$"s{bufferMemory.ScalarResource}";
|
|
return false;
|
|
}
|
|
|
|
if (bufferDescriptor.BaseAddress == 0)
|
|
{
|
|
error = $"buffer-address-null pc=0x{instruction.Pc:X}";
|
|
return false;
|
|
}
|
|
|
|
if (resolveVertexInputs &&
|
|
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
|
{
|
|
var vertexReadSize = bufferDescriptor.SizeBytes;
|
|
if (vertexRecordLimit is { } recordLimit &&
|
|
instruction.Sources.Count > 2 &&
|
|
TryEvaluateScalarOperand(
|
|
instruction.Sources[2],
|
|
scalarRegisters,
|
|
out var scalarOffset))
|
|
{
|
|
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
|
|
var requiredBytes =
|
|
(ulong)bindingOffset +
|
|
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
|
|
(ulong)bufferMemory.DwordCount * sizeof(uint);
|
|
vertexReadSize = Math.Min(
|
|
bufferDescriptor.SizeBytes,
|
|
Math.Max(requiredBytes, sizeof(uint)));
|
|
}
|
|
|
|
if (!TryReadGlobalMemory(
|
|
ctx,
|
|
bufferDescriptor.BaseAddress,
|
|
vertexReadSize,
|
|
out var vertexData))
|
|
{
|
|
error =
|
|
$"vertex-buffer-read-failed pc=0x{instruction.Pc:X} " +
|
|
$"address=0x{bufferDescriptor.BaseAddress:X16} " +
|
|
$"bytes={bufferDescriptor.SizeBytes} " +
|
|
$"stride={bufferDescriptor.Stride} records={bufferDescriptor.NumRecords}";
|
|
return false;
|
|
}
|
|
|
|
if (!TryCreateVertexInputBinding(
|
|
instruction,
|
|
bufferMemory,
|
|
bufferDescriptor,
|
|
vertexData,
|
|
(uint)vertexInputBindings.Count,
|
|
scalarRegisters,
|
|
out var vertexInputBinding))
|
|
{
|
|
error =
|
|
$"vertex-input-binding-failed pc=0x{instruction.Pc:X} " +
|
|
$"s{bufferMemory.ScalarResource}";
|
|
return false;
|
|
}
|
|
|
|
vertexInputBindings.Add(vertexInputBinding);
|
|
continue;
|
|
}
|
|
|
|
var key = (bufferMemory.ScalarResource, bufferDescriptor.BaseAddress);
|
|
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
|
{
|
|
if (existingBinding.InstructionPcs is List<uint> instructionPcs)
|
|
{
|
|
instructionPcs.Add(instruction.Pc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!TryReadGlobalMemory(
|
|
ctx,
|
|
bufferDescriptor.BaseAddress,
|
|
bufferDescriptor.SizeBytes,
|
|
out var data))
|
|
{
|
|
var descriptorWords = string.Join(
|
|
':',
|
|
Enumerable.Range(0, 4).Select(index =>
|
|
$"{scalarRegisters[bufferMemory.ScalarResource + (uint)index]:X8}"));
|
|
error =
|
|
$"buffer-memory-read-failed pc=0x{instruction.Pc:X} " +
|
|
$"address=0x{bufferDescriptor.BaseAddress:X16} " +
|
|
$"bytes={bufferDescriptor.SizeBytes} " +
|
|
$"stride={bufferDescriptor.Stride} records={bufferDescriptor.NumRecords} " +
|
|
$"s{bufferMemory.ScalarResource}=[{descriptorWords}]";
|
|
return false;
|
|
}
|
|
|
|
var binding = new Gen5GlobalMemoryBinding(
|
|
bufferMemory.ScalarResource,
|
|
bufferDescriptor.BaseAddress,
|
|
new List<uint> { instruction.Pc },
|
|
data);
|
|
globalMemoryByAddress.Add(key, binding);
|
|
globalMemoryBindings.Add(binding);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Control is not Gen5ImageControl image)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!TryCopyRegisters(
|
|
scalarRegisters,
|
|
image.ScalarResource,
|
|
ImageDescriptorDwords,
|
|
out var resourceDescriptor))
|
|
{
|
|
error = $"resource-register-range pc=0x{instruction.Pc:X} s{image.ScalarResource}";
|
|
return false;
|
|
}
|
|
|
|
IReadOnlyList<uint> samplerDescriptor = [];
|
|
if (UsesSampler(instruction.Opcode) &&
|
|
!TryCopyRegisters(
|
|
scalarRegisters,
|
|
image.ScalarSampler,
|
|
SamplerDescriptorDwords,
|
|
out samplerDescriptor))
|
|
{
|
|
error = $"sampler-register-range pc=0x{instruction.Pc:X} s{image.ScalarSampler}";
|
|
return false;
|
|
}
|
|
|
|
resolved.Add(new Gen5ImageBinding(
|
|
instruction.Pc,
|
|
instruction.Opcode,
|
|
image,
|
|
resourceDescriptor,
|
|
samplerDescriptor,
|
|
instruction.Opcode is "ImageLoadMip" or "ImageStoreMip" &&
|
|
TryResolveVectorConstantBefore(
|
|
state.Program,
|
|
instruction.Pc,
|
|
image.GetAddressRegister(2),
|
|
out var mipLevel)
|
|
? mipLevel
|
|
: null));
|
|
}
|
|
|
|
evaluation = new Gen5ShaderEvaluation(
|
|
initialScalarRegisters,
|
|
scalarRegisters,
|
|
scalarRegisterSnapshots,
|
|
resolved,
|
|
globalMemoryBindings,
|
|
state.ComputeSystemRegisters,
|
|
runtimeScalarRegisters,
|
|
vertexInputBindings);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCreateVertexInputBinding(
|
|
Gen5ShaderInstruction instruction,
|
|
Gen5BufferMemoryControl control,
|
|
BufferDescriptor descriptor,
|
|
byte[] data,
|
|
uint location,
|
|
uint[] scalarRegisters,
|
|
out Gen5VertexInputBinding binding)
|
|
{
|
|
binding = default!;
|
|
if (!IsVertexFetchCandidate(instruction, control, descriptor) ||
|
|
instruction.Sources.Count <= 2 ||
|
|
!TryEvaluateScalarOperand(instruction.Sources[2], scalarRegisters, out var scalarOffset))
|
|
{
|
|
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,
|
|
bindingStride,
|
|
bindingOffset,
|
|
bindingData);
|
|
return true;
|
|
}
|
|
|
|
private static bool IsVertexFetchCandidate(
|
|
Gen5ShaderInstruction instruction,
|
|
Gen5BufferMemoryControl control,
|
|
BufferDescriptor descriptor) =>
|
|
control.IndexEnabled &&
|
|
!control.OffsetEnabled &&
|
|
control.DwordCount is >= 1 and <= 4 &&
|
|
descriptor.BaseAddress != 0 &&
|
|
descriptor.Stride != 0 &&
|
|
(instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) ||
|
|
instruction.Opcode.StartsWith("TBufferLoadFormat", StringComparison.Ordinal));
|
|
|
|
private static HashSet<uint> CollectRuntimeScalarRegisters(Gen5ShaderProgram program)
|
|
{
|
|
var registers = new HashSet<uint>();
|
|
foreach (var instruction in program.Instructions)
|
|
{
|
|
foreach (var operand in instruction.Sources.Concat(instruction.Destinations))
|
|
{
|
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
|
operand.Value < ScalarRegisterCount)
|
|
{
|
|
registers.Add(operand.Value);
|
|
}
|
|
}
|
|
|
|
if (instruction.Control is Gen5ScalarMemoryControl
|
|
{
|
|
DynamicOffsetRegister: { } offsetRegister,
|
|
} &&
|
|
offsetRegister < ScalarRegisterCount)
|
|
{
|
|
registers.Add(offsetRegister);
|
|
}
|
|
}
|
|
|
|
return registers;
|
|
}
|
|
|
|
private static bool TryGetSoppBranchTargetPc(
|
|
Gen5ShaderInstruction instruction,
|
|
out uint targetPc)
|
|
{
|
|
targetPc = 0;
|
|
if (instruction.Encoding != Gen5ShaderEncoding.Sopp ||
|
|
instruction.Words.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var offset = unchecked((short)(instruction.Words[0] & 0xFFFF));
|
|
var nextPc = (long)instruction.Pc + instruction.Words.Count * sizeof(uint);
|
|
var target = nextPc + offset * sizeof(uint);
|
|
if (target < 0 || target > uint.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
targetPc = (uint)target;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveVectorConstantBefore(
|
|
Gen5ShaderProgram program,
|
|
uint pc,
|
|
uint vectorRegister,
|
|
out uint value)
|
|
{
|
|
for (var index = program.Instructions.Count - 1; index >= 0; index--)
|
|
{
|
|
var instruction = program.Instructions[index];
|
|
if (instruction.Pc >= pc ||
|
|
instruction.Destinations.Count != 1 ||
|
|
instruction.Destinations[0] is not
|
|
{
|
|
Kind: Gen5OperandKind.VectorRegister,
|
|
Value: var destination,
|
|
} ||
|
|
destination != vectorRegister)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Opcode == "VMovB32" &&
|
|
instruction.Sources.Count == 1 &&
|
|
TryResolveConstantOperand(instruction.Sources[0], out value))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryResolveConstantOperand(Gen5Operand operand, out uint value)
|
|
{
|
|
if (operand.Kind == Gen5OperandKind.LiteralConstant)
|
|
{
|
|
value = operand.Value;
|
|
return true;
|
|
}
|
|
|
|
if (operand.Kind == Gen5OperandKind.EncodedConstant)
|
|
{
|
|
return TryDecodeInlineConstant(operand.Value, out value);
|
|
}
|
|
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
[ThreadStatic]
|
|
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
|
|
|
public static void BeginGlobalMemoryReadScope()
|
|
{
|
|
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
|
}
|
|
|
|
public static void EndGlobalMemoryReadScope()
|
|
{
|
|
_globalMemoryReadCache = null;
|
|
}
|
|
|
|
private static bool TryReadGlobalMemory(
|
|
CpuContext ctx,
|
|
ulong baseAddress,
|
|
out byte[] data)
|
|
{
|
|
return TryReadGlobalMemory(
|
|
ctx,
|
|
baseAddress,
|
|
(ulong)DefaultGlobalMemoryBindingBytes,
|
|
out data);
|
|
}
|
|
|
|
private static bool TryReadGlobalMemory(
|
|
CpuContext ctx,
|
|
ulong baseAddress,
|
|
ulong sizeBytes,
|
|
out byte[] data)
|
|
{
|
|
if (sizeBytes == 0)
|
|
{
|
|
data = [];
|
|
return false;
|
|
}
|
|
|
|
var cappedSize = Math.Min(sizeBytes, MaxGlobalMemoryBindingBytes);
|
|
if (cappedSize > int.MaxValue)
|
|
{
|
|
data = [];
|
|
return false;
|
|
}
|
|
|
|
var cache = _globalMemoryReadCache;
|
|
var cacheKey = (baseAddress, (int)cappedSize);
|
|
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
|
|
{
|
|
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
|
|
data = cached;
|
|
return true;
|
|
}
|
|
|
|
byte[]? previous;
|
|
lock (_crossFrameReadGate)
|
|
{
|
|
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
|
|
}
|
|
|
|
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
|
|
{
|
|
Interlocked.Increment(ref GlobalMemoryReadReuses);
|
|
if (cache is not null)
|
|
{
|
|
cache[cacheKey] = previous;
|
|
}
|
|
|
|
data = previous;
|
|
return true;
|
|
}
|
|
|
|
var candidateSize = (int)cappedSize;
|
|
while (candidateSize >= sizeof(uint))
|
|
{
|
|
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
|
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
|
if (readFromPvm ||
|
|
FallbackMemoryReader?.Invoke(baseAddress, data) == true)
|
|
{
|
|
Interlocked.Increment(ref GlobalMemoryReadCount);
|
|
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
|
if (readFromPvm)
|
|
{
|
|
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
|
|
}
|
|
else
|
|
{
|
|
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
|
|
}
|
|
|
|
if (cache is not null)
|
|
{
|
|
cache[cacheKey] = data;
|
|
}
|
|
|
|
lock (_crossFrameReadGate)
|
|
{
|
|
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
|
|
{
|
|
_crossFrameReadCacheBytes -= replaced.Length;
|
|
}
|
|
|
|
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
|
|
{
|
|
_crossFrameReadCache.Clear();
|
|
_crossFrameReadCacheBytes = 0;
|
|
}
|
|
|
|
_crossFrameReadCache[cacheKey] = data;
|
|
_crossFrameReadCacheBytes += data.Length;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (candidateSize == sizeof(uint))
|
|
{
|
|
break;
|
|
}
|
|
|
|
candidateSize = Math.Max(candidateSize / 2, sizeof(uint));
|
|
}
|
|
|
|
data = [];
|
|
return false;
|
|
}
|
|
|
|
private static bool TryExecuteScalarAlu(
|
|
Gen5ShaderInstruction instruction,
|
|
ulong programAddress,
|
|
uint[] registers,
|
|
ref ulong execMask,
|
|
ref bool scalarConditionCode,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (instruction.Destinations.Count != 1 ||
|
|
instruction.Destinations[0] is not
|
|
{
|
|
Kind: Gen5OperandKind.ScalarRegister,
|
|
Value: < ScalarRegisterCount,
|
|
} destination)
|
|
{
|
|
error = $"unsupported-scalar-destination pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
if (instruction.Opcode == "SMovkI32")
|
|
{
|
|
registers[destination.Value] = unchecked((uint)(short)instruction.Sources[0].Value);
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is "SAddkI32" or "SMulkI32")
|
|
{
|
|
var immediate = unchecked((uint)(short)instruction.Sources[0].Value);
|
|
registers[destination.Value] = instruction.Opcode == "SAddkI32"
|
|
? registers[destination.Value] + immediate
|
|
: unchecked((uint)((int)registers[destination.Value] * (int)immediate));
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode == "SGetpcB64")
|
|
{
|
|
var pc = programAddress + instruction.Pc + (ulong)(instruction.Words.Count * sizeof(uint));
|
|
WriteScalarPair(registers, destination.Value, pc, ref execMask);
|
|
return true;
|
|
}
|
|
|
|
if (TryExecuteSaveExecScalarAlu(
|
|
instruction,
|
|
registers,
|
|
ref execMask,
|
|
ref scalarConditionCode,
|
|
out error))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is "SMovB64" or "SWqmB64" or "SNotB64")
|
|
{
|
|
if (destination.Value >= ScalarRegisterCount - 1 ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[0],
|
|
registers,
|
|
execMask,
|
|
out var value))
|
|
{
|
|
error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
if (instruction.Opcode == "SNotB64")
|
|
{
|
|
value = ~value;
|
|
scalarConditionCode = value != 0;
|
|
}
|
|
|
|
WriteScalarPair(registers, destination.Value, value, ref execMask);
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is "SLshlB64" or "SLshrB64")
|
|
{
|
|
if (instruction.Sources.Count < 2 ||
|
|
destination.Value >= ScalarRegisterCount - 1 ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[0],
|
|
registers,
|
|
execMask,
|
|
out var value) ||
|
|
!TryEvaluateScalarOperand(
|
|
instruction.Sources[1],
|
|
registers,
|
|
out var shift))
|
|
{
|
|
error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
value = instruction.Opcode == "SLshlB64"
|
|
? value << ((int)shift & 63)
|
|
: value >> ((int)shift & 63);
|
|
WriteScalarPair(registers, destination.Value, value, ref execMask);
|
|
scalarConditionCode = value != 0;
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is "SBfeU64" or "SBfeI64")
|
|
{
|
|
if (instruction.Sources.Count < 2 ||
|
|
destination.Value >= ScalarRegisterCount - 1 ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[0],
|
|
registers,
|
|
execMask,
|
|
out var source) ||
|
|
!TryEvaluateScalarOperand(
|
|
instruction.Sources[1],
|
|
registers,
|
|
out var control))
|
|
{
|
|
error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
var offset = (int)control & 63;
|
|
var width = Math.Min(((int)control >> 16) & 0x7F, 64 - offset);
|
|
ulong value;
|
|
if (width == 0)
|
|
{
|
|
value = 0;
|
|
}
|
|
else
|
|
{
|
|
value = source >> offset;
|
|
if (width < 64)
|
|
{
|
|
value &= ulong.MaxValue >> (64 - width);
|
|
if (instruction.Opcode == "SBfeI64")
|
|
{
|
|
value = unchecked((ulong)((long)(value << (64 - width)) >> (64 - width)));
|
|
}
|
|
}
|
|
}
|
|
|
|
WriteScalarPair(registers, destination.Value, value, ref execMask);
|
|
scalarConditionCode = value != 0;
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is
|
|
"SCselectB64" or
|
|
"SAndB64" or
|
|
"SOrB64" or
|
|
"SXorB64" or
|
|
"SAndn2B64" or
|
|
"SOrn2B64" or
|
|
"SNandB64" or
|
|
"SNorB64" or
|
|
"SXnorB64")
|
|
{
|
|
if (instruction.Sources.Count < 2 ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[0],
|
|
registers,
|
|
execMask,
|
|
out var maskLeft) ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[1],
|
|
registers,
|
|
execMask,
|
|
out var maskRight))
|
|
{
|
|
error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
var value = instruction.Opcode switch
|
|
{
|
|
"SCselectB64" => scalarConditionCode ? maskLeft : maskRight,
|
|
"SAndB64" => maskLeft & maskRight,
|
|
"SOrB64" => maskLeft | maskRight,
|
|
"SXorB64" => maskLeft ^ maskRight,
|
|
"SAndn2B64" => maskLeft & ~maskRight,
|
|
"SOrn2B64" => maskLeft | ~maskRight,
|
|
"SNandB64" => ~(maskLeft & maskRight),
|
|
"SNorB64" => ~(maskLeft | maskRight),
|
|
_ => ~(maskLeft ^ maskRight),
|
|
};
|
|
WriteScalarPair(registers, destination.Value, value, ref execMask);
|
|
scalarConditionCode = value != 0;
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Sources.Count == 0 ||
|
|
!TryEvaluateScalarOperand(instruction.Sources[0], registers, out var left))
|
|
{
|
|
var source = instruction.Sources.Count == 0
|
|
? "<missing>"
|
|
: instruction.Sources[0].ToString();
|
|
error = $"scalar-source0 pc=0x{instruction.Pc:X} op={instruction.Opcode} source={source}";
|
|
return false;
|
|
}
|
|
|
|
if (instruction.Opcode == "SMovB32")
|
|
{
|
|
registers[destination.Value] = left;
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is
|
|
"SNotB32" or
|
|
"SBrevB32" or
|
|
"SBcnt1I32B32" or
|
|
"SFF1I32B32" or
|
|
"SBitset1B32")
|
|
{
|
|
registers[destination.Value] = instruction.Opcode switch
|
|
{
|
|
"SNotB32" => ~left,
|
|
"SBrevB32" => ReverseBits(left),
|
|
"SBcnt1I32B32" => (uint)BitOperations.PopCount(left),
|
|
"SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left),
|
|
_ => registers[destination.Value] | (1u << ((int)left & 31)),
|
|
};
|
|
if (instruction.Opcode != "SBitset1B32")
|
|
{
|
|
scalarConditionCode = registers[destination.Value] != 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Sources.Count < 2 ||
|
|
!TryEvaluateScalarOperand(instruction.Sources[1], registers, out var right))
|
|
{
|
|
var source = instruction.Sources.Count < 2
|
|
? "<missing>"
|
|
: instruction.Sources[1].ToString();
|
|
error = $"scalar-source1 pc=0x{instruction.Pc:X} op={instruction.Opcode} source={source}";
|
|
return false;
|
|
}
|
|
|
|
uint result;
|
|
switch (instruction.Opcode)
|
|
{
|
|
case "SAddU32":
|
|
{
|
|
var wide = (ulong)left + right;
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SSubU32":
|
|
result = 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":
|
|
{
|
|
var wide = (ulong)left + right + (scalarConditionCode ? 1UL : 0UL);
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SSubbU32":
|
|
{
|
|
var borrow = scalarConditionCode ? 1UL : 0UL;
|
|
var subtrahend = (ulong)right + borrow;
|
|
result = unchecked(left - (uint)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;
|
|
break;
|
|
case "SAndB32":
|
|
result = left & right;
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SOrB32":
|
|
result = left | right;
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SXorB32":
|
|
result = left ^ right;
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SAndn2B32":
|
|
result = left & ~right;
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SOrn2B32":
|
|
result = left | ~right;
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SNandB32":
|
|
result = ~(left & right);
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SNorB32":
|
|
result = ~(left | right);
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SXnorB32":
|
|
result = ~(left ^ right);
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SLshlB32":
|
|
result = left << ((int)right & 31);
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SLshrB32":
|
|
result = left >> ((int)right & 31);
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SAshrI32":
|
|
result = unchecked((uint)((int)left >> ((int)right & 31)));
|
|
scalarConditionCode = result != 0;
|
|
break;
|
|
case "SBfmB32":
|
|
{
|
|
var width = (int)left & 31;
|
|
var offset = (int)right & 31;
|
|
result = width == 0 ? 0 : ((1u << width) - 1u) << offset;
|
|
break;
|
|
}
|
|
case "SMulI32":
|
|
result = unchecked((uint)((int)left * (int)right));
|
|
break;
|
|
case "SBfeU32":
|
|
{
|
|
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":
|
|
{
|
|
var offset = (int)right & 31;
|
|
var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset);
|
|
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":
|
|
{
|
|
var wide = ((ulong)left << 1) + right;
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SLshl2AddU32":
|
|
{
|
|
var wide = ((ulong)left << 2) + right;
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SLshl3AddU32":
|
|
{
|
|
var wide = ((ulong)left << 3) + right;
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SLshl4AddU32":
|
|
{
|
|
var wide = ((ulong)left << 4) + right;
|
|
result = (uint)wide;
|
|
scalarConditionCode = wide > uint.MaxValue;
|
|
break;
|
|
}
|
|
case "SPackLlB32B16":
|
|
result = (left & 0xFFFFu) | (right << 16);
|
|
break;
|
|
case "SPackLhB32B16":
|
|
result = (left & 0xFFFFu) | (right & 0xFFFF0000u);
|
|
break;
|
|
case "SPackHhB32B16":
|
|
result = (left >> 16) | (right & 0xFFFF0000u);
|
|
break;
|
|
case "SMulHiU32":
|
|
result = (uint)(((ulong)left * right) >> 32);
|
|
break;
|
|
case "SMulHiI32":
|
|
result = unchecked((uint)(((long)(int)left * (int)right) >> 32));
|
|
break;
|
|
default:
|
|
error = $"unsupported-scalar-op pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
registers[destination.Value] = result;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryExecuteSaveExecScalarAlu(
|
|
Gen5ShaderInstruction instruction,
|
|
uint[] registers,
|
|
ref ulong execMask,
|
|
ref bool scalarConditionCode,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (instruction.Opcode is not (
|
|
"SAndSaveexecB64" or
|
|
"SOrSaveexecB64" or
|
|
"SXorSaveexecB64" or
|
|
"SAndn2SaveexecB64" or
|
|
"SOrn2SaveexecB64" or
|
|
"SNandSaveexecB64" or
|
|
"SNorSaveexecB64" or
|
|
"SXnorSaveexecB64" or
|
|
"SAndn1SaveexecB64" or
|
|
"SOrn1SaveexecB64"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (instruction.Destinations.Count != 1 ||
|
|
instruction.Destinations[0] is not
|
|
{
|
|
Kind: Gen5OperandKind.ScalarRegister,
|
|
Value: < ScalarRegisterCount - 1,
|
|
} destination ||
|
|
instruction.Sources.Count == 0 ||
|
|
!TryEvaluateScalarOperand64(
|
|
instruction.Sources[0],
|
|
registers,
|
|
execMask,
|
|
out var source))
|
|
{
|
|
error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
var oldExec = execMask;
|
|
var newExec = instruction.Opcode switch
|
|
{
|
|
"SAndSaveexecB64" => oldExec & source,
|
|
"SOrSaveexecB64" => oldExec | source,
|
|
"SXorSaveexecB64" => oldExec ^ source,
|
|
"SAndn1SaveexecB64" => ~source & oldExec,
|
|
"SAndn2SaveexecB64" => source & ~oldExec,
|
|
"SOrn1SaveexecB64" => ~source | oldExec,
|
|
"SOrn2SaveexecB64" => source | ~oldExec,
|
|
"SNandSaveexecB64" => ~(source & oldExec),
|
|
"SNorSaveexecB64" => ~(source | oldExec),
|
|
_ => ~(oldExec ^ source),
|
|
};
|
|
|
|
WriteScalarPair(registers, destination.Value, oldExec, ref execMask);
|
|
execMask = MaskWaveValue(newExec);
|
|
WriteScalarPair(registers, 126, execMask, ref execMask);
|
|
scalarConditionCode = execMask != 0;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryEvaluateScalarOperand64(
|
|
Gen5Operand operand,
|
|
uint[] registers,
|
|
ulong execMask,
|
|
out ulong value)
|
|
{
|
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
|
operand.Value == 126)
|
|
{
|
|
value = execMask;
|
|
return true;
|
|
}
|
|
|
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
|
operand.Value < ScalarRegisterCount - 1)
|
|
{
|
|
value = registers[operand.Value] | ((ulong)registers[operand.Value + 1] << 32);
|
|
return true;
|
|
}
|
|
|
|
if (TryEvaluateScalarOperand(operand, registers, out var low))
|
|
{
|
|
value = operand.Kind == Gen5OperandKind.EncodedConstant &&
|
|
operand.Value is >= 193 and <= 208
|
|
? ulong.MaxValue << 32 | low
|
|
: low;
|
|
return true;
|
|
}
|
|
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
private static void WriteScalarPair(
|
|
uint[] registers,
|
|
uint destination,
|
|
ulong value,
|
|
ref ulong execMask)
|
|
{
|
|
if (destination >= ScalarRegisterCount - 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (destination == 126)
|
|
{
|
|
value = MaskWaveValue(value);
|
|
}
|
|
|
|
registers[destination] = (uint)value;
|
|
registers[destination + 1] = (uint)(value >> 32);
|
|
if (destination == 126)
|
|
{
|
|
execMask = value;
|
|
}
|
|
}
|
|
|
|
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,
|
|
out bool scalarConditionCode,
|
|
out string error)
|
|
{
|
|
scalarConditionCode = false;
|
|
error = string.Empty;
|
|
if (instruction.Sources.Count != 2 ||
|
|
!TryEvaluateScalarOperand(instruction.Sources[0], registers, out var left) ||
|
|
!TryEvaluateScalarOperand(instruction.Sources[1], registers, out var right))
|
|
{
|
|
error = $"scalar-compare-source pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
if (instruction.Opcode is "SBitcmp0B32" or "SBitcmp1B32")
|
|
{
|
|
var bit = (int)(right & 31u);
|
|
var isSet = ((left >> bit) & 1u) != 0;
|
|
scalarConditionCode = instruction.Opcode == "SBitcmp1B32" ? isSet : !isSet;
|
|
return true;
|
|
}
|
|
|
|
if (instruction.Opcode is "SBitcmp0B64" or "SBitcmp1B64")
|
|
{
|
|
if (!TryEvaluateScalarOperand64(instruction.Sources[0], registers, ulong.MaxValue, out var wide))
|
|
{
|
|
error = $"scalar-bitcmp-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
var bit = (int)(right & 63u);
|
|
var isSet = ((wide >> bit) & 1UL) != 0;
|
|
scalarConditionCode = instruction.Opcode == "SBitcmp1B64" ? isSet : !isSet;
|
|
return true;
|
|
}
|
|
|
|
scalarConditionCode = instruction.Opcode switch
|
|
{
|
|
"SCmpEqI32" => (int)left == (int)right,
|
|
"SCmpLgI32" => (int)left != (int)right,
|
|
"SCmpGtI32" => (int)left > (int)right,
|
|
"SCmpGeI32" => (int)left >= (int)right,
|
|
"SCmpLtI32" => (int)left < (int)right,
|
|
"SCmpLeI32" => (int)left <= (int)right,
|
|
"SCmpEqU32" => left == right,
|
|
"SCmpLgU32" => left != right,
|
|
"SCmpGtU32" => left > right,
|
|
"SCmpGeU32" => left >= right,
|
|
"SCmpLtU32" => left < right,
|
|
"SCmpLeU32" => left <= right,
|
|
_ => false,
|
|
};
|
|
if (!instruction.Opcode.StartsWith("SCmp", StringComparison.Ordinal))
|
|
{
|
|
error = $"unsupported-scalar-compare pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryExecuteScalarCompareK(
|
|
Gen5ShaderInstruction instruction,
|
|
uint[] registers,
|
|
out bool scalarConditionCode,
|
|
out string error)
|
|
{
|
|
scalarConditionCode = false;
|
|
error = string.Empty;
|
|
if (instruction.Destinations.Count != 1 ||
|
|
instruction.Destinations[0] is not
|
|
{
|
|
Kind: Gen5OperandKind.ScalarRegister,
|
|
Value: < ScalarRegisterCount,
|
|
} destination)
|
|
{
|
|
error = $"scalar-comparek-destination pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
var left = registers[destination.Value];
|
|
var right = unchecked((uint)(short)instruction.Sources[0].Value);
|
|
scalarConditionCode = instruction.Opcode switch
|
|
{
|
|
"SCmpkEqI32" => (int)left == (int)right,
|
|
"SCmpkLgI32" => (int)left != (int)right,
|
|
"SCmpkGtI32" => (int)left > (int)right,
|
|
"SCmpkGeI32" => (int)left >= (int)right,
|
|
"SCmpkLtI32" => (int)left < (int)right,
|
|
"SCmpkLeI32" => (int)left <= (int)right,
|
|
"SCmpkEqU32" => left == right,
|
|
"SCmpkLgU32" => left != right,
|
|
"SCmpkGtU32" => left > right,
|
|
"SCmpkGeU32" => left >= right,
|
|
"SCmpkLtU32" => left < right,
|
|
"SCmpkLeU32" => left <= right,
|
|
_ => false,
|
|
};
|
|
if (!instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal))
|
|
{
|
|
error = $"unsupported-scalar-comparek pc=0x{instruction.Pc:X} op={instruction.Opcode}";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryExecuteScalarLoad(
|
|
CpuContext ctx,
|
|
Gen5ShaderState state,
|
|
Gen5ShaderInstruction instruction,
|
|
Gen5ScalarMemoryControl control,
|
|
uint[] scalarRegisters,
|
|
List<Gen5GlobalMemoryBinding> globalMemoryBindings,
|
|
Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding> globalMemoryByAddress,
|
|
HashSet<uint> runtimeScalarRegisters,
|
|
out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (instruction.Sources.Count == 0 ||
|
|
instruction.Sources[0] is not
|
|
{
|
|
Kind: Gen5OperandKind.ScalarRegister,
|
|
Value: < ScalarRegisterCount - 1,
|
|
} scalarBase)
|
|
{
|
|
error = $"invalid-scalar-base pc=0x{instruction.Pc:X}";
|
|
return false;
|
|
}
|
|
|
|
var isBufferLoad =
|
|
instruction.Opcode.StartsWith("SBufferLoad", StringComparison.Ordinal);
|
|
BufferDescriptor bufferDescriptor = default;
|
|
var hasBufferDescriptor =
|
|
isBufferLoad &&
|
|
TryDecodeBufferDescriptor(
|
|
scalarRegisters,
|
|
scalarBase.Value,
|
|
strictType: false,
|
|
out bufferDescriptor);
|
|
var baseAddress = hasBufferDescriptor
|
|
? bufferDescriptor.BaseAddress
|
|
: scalarRegisters[scalarBase.Value] |
|
|
((ulong)scalarRegisters[scalarBase.Value + 1] << 32);
|
|
var dynamicOffset = control.DynamicOffsetRegister is { } offsetRegister &&
|
|
offsetRegister < ScalarRegisterCount
|
|
? scalarRegisters[offsetRegister]
|
|
: 0;
|
|
var immediateOffset = (ulong)(long)control.ImmediateOffsetBytes;
|
|
var byteOffset = unchecked(immediateOffset + dynamicOffset);
|
|
var address = unchecked(
|
|
baseAddress +
|
|
byteOffset) & ~3UL;
|
|
var bufferUnbound =
|
|
isBufferLoad &&
|
|
(!hasBufferDescriptor ||
|
|
bufferDescriptor.SizeBytes == 0 ||
|
|
(scalarRegisters[scalarBase.Value] == 0 &&
|
|
scalarRegisters[scalarBase.Value + 1] == 0 &&
|
|
scalarBase.Value + 3 < ScalarRegisterCount &&
|
|
scalarRegisters[scalarBase.Value + 2] == 0 &&
|
|
scalarRegisters[scalarBase.Value + 3] == 0));
|
|
var bufferSize = ulong.MaxValue;
|
|
if (isBufferLoad)
|
|
{
|
|
bufferSize = hasBufferDescriptor ? bufferDescriptor.SizeBytes : ulong.MaxValue;
|
|
|
|
var key = (scalarBase.Value, bufferDescriptor.BaseAddress);
|
|
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
|
{
|
|
if (existingBinding.InstructionPcs is List<uint> instructionPcs) instructionPcs.Add(instruction.Pc);
|
|
}
|
|
else
|
|
{
|
|
TryReadGlobalMemory(ctx, bufferDescriptor.BaseAddress, bufferDescriptor.SizeBytes, out var data);
|
|
var binding = new Gen5GlobalMemoryBinding(scalarBase.Value, bufferDescriptor.BaseAddress, new List<uint> { instruction.Pc }, data);
|
|
globalMemoryByAddress.Add(key, binding);
|
|
globalMemoryBindings.Add(binding);
|
|
}
|
|
}
|
|
else if (baseAddress != 0)
|
|
{
|
|
var key = (scalarBase.Value, baseAddress);
|
|
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
|
{
|
|
if (existingBinding.InstructionPcs is List<uint> instructionPcs)
|
|
{
|
|
instructionPcs.Add(instruction.Pc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var requiredBytes = Math.Max(
|
|
256UL * 1024UL,
|
|
checked(
|
|
byteOffset +
|
|
(ulong)Math.Max(instruction.Destinations.Count, 1) *
|
|
sizeof(uint)));
|
|
requiredBytes = Math.Min(
|
|
(requiredBytes + 4095UL) & ~4095UL,
|
|
MaxGlobalMemoryBindingBytes);
|
|
if (!TryReadGlobalMemory(
|
|
ctx,
|
|
baseAddress,
|
|
requiredBytes,
|
|
out var data))
|
|
{
|
|
data = [];
|
|
}
|
|
|
|
var binding = new Gen5GlobalMemoryBinding(
|
|
scalarBase.Value,
|
|
baseAddress,
|
|
new List<uint> { instruction.Pc },
|
|
data);
|
|
globalMemoryByAddress.Add(key, binding);
|
|
globalMemoryBindings.Add(binding);
|
|
}
|
|
}
|
|
|
|
if (!bufferUnbound && address == 0)
|
|
{
|
|
error = FormatScalarLoadError(
|
|
"invalid-load-address",
|
|
instruction,
|
|
scalarBase.Value,
|
|
scalarRegisters,
|
|
control,
|
|
baseAddress,
|
|
dynamicOffset,
|
|
address);
|
|
return false;
|
|
}
|
|
|
|
for (var index = 0; index < instruction.Destinations.Count; index++)
|
|
{
|
|
var destination = instruction.Destinations[index];
|
|
if (destination.Kind != Gen5OperandKind.ScalarRegister ||
|
|
destination.Value >= ScalarRegisterCount)
|
|
{
|
|
error = FormatScalarLoadError(
|
|
"invalid-scalar-destination",
|
|
instruction,
|
|
scalarBase.Value,
|
|
scalarRegisters,
|
|
control,
|
|
baseAddress,
|
|
dynamicOffset,
|
|
address);
|
|
return false;
|
|
}
|
|
|
|
var componentOffset = unchecked(byteOffset + (ulong)(index * sizeof(uint)));
|
|
if (bufferUnbound ||
|
|
isBufferLoad &&
|
|
(componentOffset >= bufferSize ||
|
|
bufferSize - componentOffset < sizeof(uint)))
|
|
{
|
|
scalarRegisters[destination.Value] = 0;
|
|
continue;
|
|
}
|
|
|
|
if (!ctx.TryReadUInt32(
|
|
address + (ulong)(index * sizeof(uint)),
|
|
out var value) &&
|
|
!TryReadUserDataScalarLoad(
|
|
state,
|
|
instruction,
|
|
control,
|
|
byteOffset,
|
|
index,
|
|
out value))
|
|
{
|
|
if (isBufferLoad)
|
|
{
|
|
scalarRegisters[destination.Value] = 0;
|
|
continue;
|
|
}
|
|
|
|
error = FormatScalarLoadError(
|
|
"scalar-load-failed",
|
|
instruction,
|
|
scalarBase.Value,
|
|
scalarRegisters,
|
|
control,
|
|
baseAddress,
|
|
dynamicOffset,
|
|
address);
|
|
return false;
|
|
}
|
|
|
|
scalarRegisters[destination.Value] = value;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryDecodeBufferDescriptor(
|
|
IReadOnlyList<uint> scalarRegisters,
|
|
uint scalarBase,
|
|
bool strictType,
|
|
out BufferDescriptor descriptor)
|
|
{
|
|
descriptor = default;
|
|
if (scalarBase + 3 >= scalarRegisters.Count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var word0 = scalarRegisters[(int)scalarBase];
|
|
var word1 = scalarRegisters[(int)scalarBase + 1];
|
|
var word2 = scalarRegisters[(int)scalarBase + 2];
|
|
var word3 = scalarRegisters[(int)scalarBase + 3];
|
|
if (word0 == 0 &&
|
|
word1 == 0 &&
|
|
word2 == 0 &&
|
|
word3 == 0)
|
|
{
|
|
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
|
|
return true;
|
|
}
|
|
|
|
var type = word3 >> 30;
|
|
if (type != 0)
|
|
{
|
|
if (strictType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
|
|
return true;
|
|
}
|
|
|
|
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, 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,
|
|
Gen5ScalarMemoryControl control,
|
|
ulong byteOffset,
|
|
int componentIndex,
|
|
out uint value)
|
|
{
|
|
value = 0;
|
|
if (!instruction.Opcode.StartsWith("SLoadDword", StringComparison.Ordinal) ||
|
|
state.Metadata is not { } metadata ||
|
|
(byteOffset & 3) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var baseDwordOffset = byteOffset >> 2;
|
|
if (baseDwordOffset > int.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var dwordOffset = (long)baseDwordOffset + componentIndex;
|
|
if (dwordOffset < 0 ||
|
|
dwordOffset >= state.UserData.Count ||
|
|
!IsShaderUserDataResourceOffset(metadata, (uint)dwordOffset))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = state.UserData[(int)dwordOffset];
|
|
return true;
|
|
}
|
|
|
|
private static bool IsShaderUserDataResourceOffset(
|
|
Gen5ShaderMetadata metadata,
|
|
uint dwordOffset)
|
|
{
|
|
if (dwordOffset < metadata.ShaderResourceTableSizeDwords)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
foreach (var resource in metadata.Resources)
|
|
{
|
|
var dwordCount = resource.Kind switch
|
|
{
|
|
Gen5ShaderResourceKind.ReadOnlyTexture or
|
|
Gen5ShaderResourceKind.ReadWriteTexture => 8u,
|
|
Gen5ShaderResourceKind.Sampler or
|
|
Gen5ShaderResourceKind.ConstantBuffer => 4u,
|
|
_ => 1u,
|
|
};
|
|
if (dwordOffset >= resource.OffsetDwords &&
|
|
dwordOffset < resource.OffsetDwords + dwordCount)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return metadata.DirectResources.Values.Any(offset => offset == dwordOffset);
|
|
}
|
|
|
|
private static string FormatScalarLoadError(
|
|
string reason,
|
|
Gen5ShaderInstruction instruction,
|
|
uint scalarBase,
|
|
uint[] scalarRegisters,
|
|
Gen5ScalarMemoryControl control,
|
|
ulong baseAddress,
|
|
uint dynamicOffset,
|
|
ulong address)
|
|
{
|
|
var high = scalarBase + 1 < scalarRegisters.Length
|
|
? scalarRegisters[scalarBase + 1]
|
|
: 0;
|
|
var dynamic = control.DynamicOffsetRegister is { } register
|
|
? $" dyn=s{register}=0x{dynamicOffset:X8}"
|
|
: " dyn=none";
|
|
var descriptor = string.Join(
|
|
':',
|
|
Enumerable.Range(0, 4).Select(index =>
|
|
scalarBase + index < scalarRegisters.Length
|
|
? $"{scalarRegisters[scalarBase + index]:X8}"
|
|
: "????????"));
|
|
var words = string.Join(',', instruction.Words.Select(word => $"{word:X8}"));
|
|
return
|
|
$"{reason} pc=0x{instruction.Pc:X} op={instruction.Opcode} " +
|
|
$"words=[{words}] base=s{scalarBase}[0x{scalarRegisters[scalarBase]:X8}:0x{high:X8}] " +
|
|
$"desc=[{descriptor}] " +
|
|
$"base_addr=0x{baseAddress:X16} imm={control.ImmediateOffsetBytes}" +
|
|
$"{dynamic} address=0x{address:X16}";
|
|
}
|
|
|
|
private static bool TryEvaluateScalarOperand(
|
|
Gen5Operand operand,
|
|
uint[] scalarRegisters,
|
|
out uint value)
|
|
{
|
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
|
operand.Value < ScalarRegisterCount)
|
|
{
|
|
value = scalarRegisters[operand.Value];
|
|
return true;
|
|
}
|
|
|
|
if (operand.Kind == Gen5OperandKind.LiteralConstant)
|
|
{
|
|
value = operand.Value;
|
|
return true;
|
|
}
|
|
|
|
if (operand.Kind == Gen5OperandKind.EncodedConstant)
|
|
{
|
|
return TryDecodeInlineConstant(operand.Value, out value);
|
|
}
|
|
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryDecodeInlineConstant(uint encoded, out uint value)
|
|
{
|
|
if (encoded == 125)
|
|
{
|
|
value = 0;
|
|
return true;
|
|
}
|
|
|
|
if (encoded is >= 128 and <= 192)
|
|
{
|
|
value = encoded - 128;
|
|
return true;
|
|
}
|
|
|
|
if (encoded is >= 193 and <= 208)
|
|
{
|
|
value = unchecked((uint)-(int)(encoded - 192));
|
|
return true;
|
|
}
|
|
|
|
var floatingPoint = encoded switch
|
|
{
|
|
240 => 0.5f,
|
|
241 => -0.5f,
|
|
242 => 1.0f,
|
|
243 => -1.0f,
|
|
244 => 2.0f,
|
|
245 => -2.0f,
|
|
246 => 4.0f,
|
|
247 => -4.0f,
|
|
248 => 1.0f / (2.0f * MathF.PI),
|
|
_ => float.NaN,
|
|
};
|
|
if (float.IsNaN(floatingPoint))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BitConverter.SingleToUInt32Bits(floatingPoint);
|
|
return true;
|
|
}
|
|
|
|
private static uint ReverseBits(uint value)
|
|
{
|
|
value = (value >> 1 & 0x55555555u) | ((value & 0x55555555u) << 1);
|
|
value = (value >> 2 & 0x33333333u) | ((value & 0x33333333u) << 2);
|
|
value = (value >> 4 & 0x0F0F0F0Fu) | ((value & 0x0F0F0F0Fu) << 4);
|
|
value = (value >> 8 & 0x00FF00FFu) | ((value & 0x00FF00FFu) << 8);
|
|
return value >> 16 | value << 16;
|
|
}
|
|
|
|
private static bool TryCopyRegisters(
|
|
uint[] registers,
|
|
uint start,
|
|
int count,
|
|
out IReadOnlyList<uint> values)
|
|
{
|
|
values = [];
|
|
if (start > (uint)(registers.Length - count))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var copy = new uint[count];
|
|
Array.Copy(registers, (int)start, copy, 0, count);
|
|
values = copy;
|
|
return true;
|
|
}
|
|
|
|
private static bool UsesSampler(string opcode) =>
|
|
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
|
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
|
|
|
}
|