Compare commits
8 Commits
1016_part3
...
fix/render
| Author | SHA1 | Date |
|---|---|---|
|
|
3b12e429a9 | |
|
|
52ba493155 | |
|
|
829cf4132e | |
|
|
aef84c8934 | |
|
|
802c8fc6e4 | |
|
|
c52d31d73b | |
|
|
ecf019f23b | |
|
|
e65b2e8233 |
|
|
@ -703,11 +703,39 @@ public sealed unsafe partial class GuestImageCache
|
|||
}
|
||||
}
|
||||
|
||||
// The mip layout can end before the tile plan does (Silent Hill: a 128x128 slice planned at
|
||||
// the exact end of the mip chain aborted the process). Cover the whole plan when the buffer
|
||||
// holds it; otherwise keep the guest bytes instead of copying a partial image.
|
||||
if (!plan.Depth && plan.Color.Tiled)
|
||||
{
|
||||
var tiledEnd = TiledEndOf(plan.Color.Tiles);
|
||||
if (tiledEnd > copySize)
|
||||
{
|
||||
if (tiledEnd > available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
copySize = tiledEnd;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
|
||||
image.MarkBufferHoldsGpuContents();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong TiledEndOf(List<TileTransfer> tiles)
|
||||
{
|
||||
var end = 0UL;
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
end = Math.Max(end, tile.TiledOffset + tile.TiledSize);
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
// Publishes a GPU-owned image to guest memory after its tick completes; false when it cannot.
|
||||
private bool TryDownloadToGuest(ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -585,8 +585,13 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var view = binding.Request.View;
|
||||
for (var layer = view.BaseLayer; layer < view.BaseLayer + view.LayerCount; layer++)
|
||||
{
|
||||
var tracked = _imageCache.IsMetadataCleared(metadataAddress, layer, out var metadataValue) && (byte)metadataValue == DccClearToZero;
|
||||
// Every fixed clear code carries its own colour; only the register code (0x20) needs
|
||||
// the CB clear word, which is known when the surface binds as a target. Silent Hill
|
||||
// fast-clears UE's separate-translucency target to (0,0,0,1) and only samples it:
|
||||
// skipping that clear left stale zeros that blacked out the whole title-menu scene.
|
||||
var clearValue = default(ClearColorValue);
|
||||
var tracked = _imageCache.IsMetadataCleared(metadataAddress, layer, out var metadataValue) &&
|
||||
TryDecodeDccClear((byte)metadataValue, false, (byte)metadataValue == DccClearToZero || fixedClearSupported, default, out clearValue);
|
||||
ulong guestSlice = 0;
|
||||
if (!tracked &&
|
||||
(sliceSize == 0 || !_imageCache.TryReadGuestDccClear(metadataAddress, sliceSize, layer, out guestSlice, out var code) ||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private void CreateImageCache()
|
||||
{
|
||||
var (memory, _, backing) = RequireGuestMemory("image store");
|
||||
_imageCache = new GuestImageCache(_deviceInfo, _scheduler, memory.Pages, _bufferCache, backing, readbackLinearImages: false);
|
||||
// The guest reads linear GPU-written images back on the CPU (UE's exposure readback
|
||||
// feeds View.PreExposure), so they are published to guest memory after their tick.
|
||||
// SHARPEMU_READBACK_LINEAR_IMAGES=0 keeps them GPU-only.
|
||||
_imageCache = new GuestImageCache(_deviceInfo, _scheduler, memory.Pages, _bufferCache, backing,
|
||||
readbackLinearImages: Environment.GetEnvironmentVariable("SHARPEMU_READBACK_LINEAR_IMAGES") != "0");
|
||||
_bufferCache.ImageCache = _imageCache;
|
||||
_samplerStore = new SamplerStore(_deviceInfo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1778,8 +1778,11 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
|
||||
// register operand is taken as an f16 (the half picked by op_sel, widened
|
||||
// exactly to f32) or as a full f32; inline constants are always f32. The
|
||||
// register or literal operand is taken as an f16 (the half picked by op_sel,
|
||||
// widened exactly to f32) or as a full f32; inline constants keep their value
|
||||
// either way. A literal is an f16 bit pattern then: reading 0x34CD as an f32
|
||||
// made Silent Hill's luminance weights denormal zeros, and the lighting that
|
||||
// divides by that luminance came out thousands of times too bright. The
|
||||
// per-operand neg_hi bit takes the absolute value and neg negates, in that
|
||||
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
|
||||
// ops define them rather than the packed low/high-lane meaning.
|
||||
|
|
@ -1791,7 +1794,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var source = instruction.Sources[index];
|
||||
var readAsHalf =
|
||||
((control.OpSelHiMask >> index) & 1) != 0 &&
|
||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
|
||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister or Gen5OperandKind.LiteralConstant;
|
||||
|
||||
uint value;
|
||||
if (readAsHalf)
|
||||
|
|
@ -5199,12 +5202,33 @@ public static partial class Gen5SpirvTranslator
|
|||
return Bitcast(_uintType, value);
|
||||
}
|
||||
|
||||
// Rounds an f32 toward zero onto an f16 value, so packHalf2x16 then encodes it exactly
|
||||
// (V_CVT_PKRTZ_F16_F32). Toward zero, a finite value beyond the f16 range becomes
|
||||
// +-65504, never infinity: Silent Hill's deferred lighting packs values past 65504 and
|
||||
// its fog pass then scales them down, while an infinity stays infinite, turns into NaN
|
||||
// and blacks out the whole title-menu scene. Subnormal results keep whole 2^-24 steps.
|
||||
private uint TruncateFloat32ForPack(uint value)
|
||||
{
|
||||
var raw = BitwiseAnd(
|
||||
Bitcast(_uintType, value),
|
||||
UInt(0xFFFF_E000));
|
||||
return Bitcast(_floatType, raw);
|
||||
var normal = Bitcast(
|
||||
_floatType,
|
||||
BitwiseAnd(Bitcast(_uintType, Ext(43, _floatType, value, Float(-65504f), Float(65504f))), UInt(0xFFFF_E000)));
|
||||
var subnormal = _module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
Ext(3, _floatType, _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(16777216f))),
|
||||
Float(1f / 16777216f));
|
||||
var isSubnormal = _module.AddInstruction(
|
||||
SpirvOp.FOrdLessThan,
|
||||
_boolType,
|
||||
Ext(4, _floatType, value),
|
||||
Float(6.10351562e-05f));
|
||||
var finite = _module.AddInstruction(SpirvOp.Select, _floatType, isSubnormal, subnormal, normal);
|
||||
var special = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, value),
|
||||
_module.AddInstruction(SpirvOp.IsInf, _boolType, value));
|
||||
return _module.AddInstruction(SpirvOp.Select, _floatType, special, value, finite);
|
||||
}
|
||||
|
||||
private uint Ext(uint operation, uint resultType, params uint[] operands)
|
||||
|
|
|
|||
|
|
@ -1453,9 +1453,11 @@ public static partial class Gen5SpirvTranslator
|
|||
var block = blocks[blockIndex];
|
||||
var halfMaskPlan = HalfMaskPlan();
|
||||
// One guest wave can span two host subgroups. Keep its shared-memory phases ordered.
|
||||
// The half-mask plan inserts its own barriers; other wave64 programs also
|
||||
// need LDS phase ordering when reads and writes span basic blocks.
|
||||
var synchronizeSharedMemory = _emulateWave64 && halfMaskPlan is null;
|
||||
// The half-mask plan's barriers only order its own mask exchanges, not the program's
|
||||
// LDS traffic: with phase ordering off under the plan, UE's FFT bloom (wave64, 512
|
||||
// threads, LDS butterflies) read half-written rows and blurred Silent Hill's whole
|
||||
// post chain into a flat haze.
|
||||
var synchronizeSharedMemory = _emulateWave64;
|
||||
var sharedMemoryPhase = SharedMemoryPhase.None;
|
||||
for (var index = block.StartIndex; index < block.EndIndex; index++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,17 +48,25 @@ public sealed class CommandStreamWorkerTests
|
|||
[Fact]
|
||||
public void Worker_RetriesABlockedHeadAfterTheTimedWait()
|
||||
{
|
||||
var (host, queue, worker) = NewWorker(cancelBlockedAtStop: false);
|
||||
var (host, queue, worker) = NewWorker(cancelBlockedAtStop: true);
|
||||
host.WriteDword(Label, 0);
|
||||
|
||||
Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6));
|
||||
Thread.Sleep(50);
|
||||
host.WriteDword(Label, 1);
|
||||
try
|
||||
{
|
||||
Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6));
|
||||
// A loaded runner can start the worker after a fixed sleep has elapsed.
|
||||
// Publish the signal only once the worker has actually blocked on it.
|
||||
Assert.True(SpinWait.SpinUntil(() => queue.BlockedQueueCount == 1, TimeSpan.FromSeconds(5)));
|
||||
host.WriteDword(Label, 1);
|
||||
|
||||
Assert.Equal(IdleOutcome.Completed, queue.WaitForIdle());
|
||||
Assert.Equal(6u, queue.GetInterpreter(0).InstanceCount);
|
||||
Assert.True(queue.BlockedRetries >= 1);
|
||||
worker.Stop();
|
||||
Assert.Equal(IdleOutcome.Completed, queue.WaitForIdle());
|
||||
Assert.Equal(6u, queue.GetInterpreter(0).InstanceCount);
|
||||
Assert.True(queue.BlockedRetries >= 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
worker.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -122,5 +122,106 @@ public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : I
|
|||
}
|
||||
}
|
||||
|
||||
// op_sel_hi marks a V_FMA_MIX operand as f16, literals included: 0x34CD is 0.3, not an
|
||||
// f32 denormal. Silent Hill weights a luminance with such literals and divides by it.
|
||||
[Fact]
|
||||
public void FusedMixReadsHalfSelectedLiteralsAsHalfFloats()
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
var halfLiteral = new Gen5Vop3pControl(0, 0b001, 0, 0, false);
|
||||
var floatLiteral = new Gen5Vop3pControl(0, 0b000, 0, 0, false);
|
||||
var program = Program(
|
||||
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)),
|
||||
Vop3(4, "VFmaMixF32", 12, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x34CD), Gen5Operand.Vector(1), Operand(0)) with { Control = halfLiteral },
|
||||
Vop3(16, "VFmaMixF32", 13, new Gen5Operand(Gen5OperandKind.LiteralConstant, Bits(0.25f)), Gen5Operand.Vector(1), Operand(0)) with { Control = floatLiteral },
|
||||
Vop2(28, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
|
||||
BufferAccess(32, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7),
|
||||
BufferAccess(40, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7),
|
||||
EndProgram(48));
|
||||
var (plan, resources, layout) = Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
using var harness = new ImageTestHarness(vulkan);
|
||||
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
|
||||
var result = runner.CreateBuffer(ThreadCount * RowBytes);
|
||||
var registers = new uint[256];
|
||||
registers[6] = ThreadCount * RowBytes;
|
||||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
|
||||
var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes);
|
||||
var weight = (float)BitConverter.UInt16BitsToHalf(0x34CD);
|
||||
for (var lane = 0u; lane < ThreadCount; lane++)
|
||||
{
|
||||
var row = actual.AsSpan((int)(lane * RowBytes));
|
||||
Assert.Equal(Bits(MathF.FusedMultiplyAdd(weight, lane, 0f)), BinaryPrimitives.ReadUInt32LittleEndian(row));
|
||||
Assert.Equal(Bits(lane * 0.25f), BinaryPrimitives.ReadUInt32LittleEndian(row[4..]));
|
||||
}
|
||||
}
|
||||
|
||||
// V_CVT_PKRTZ_F16_F32 rounds toward zero: finite values past the f16 range saturate at
|
||||
// +-65504 instead of becoming infinity, and subnormal results keep whole 2^-24 steps.
|
||||
[Fact]
|
||||
public void PackToHalfRoundsTowardZeroWithoutOverflowingToInfinity()
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
var program = Program(
|
||||
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)),
|
||||
MoveScalar(4, 8, Bits(5003.7f)),
|
||||
MoveScalar(12, 9, Bits(-80000.3f)),
|
||||
MoveScalar(20, 10, Bits(3.3e-6f)),
|
||||
MoveScalar(28, 11, Bits(-5e-5f)),
|
||||
MoveScalar(36, 12, Bits(float.NaN)),
|
||||
MoveScalar(44, 13, Bits(float.PositiveInfinity)),
|
||||
Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)),
|
||||
Vop3(60, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)),
|
||||
Vop2(68, "VCvtPkrtzF16F32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(3)),
|
||||
Vop3(72, "VCvtPkrtzF16F32", 13, Gen5Operand.Scalar(12), Gen5Operand.Scalar(13)),
|
||||
Vop2(80, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
|
||||
BufferAccess(84, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7),
|
||||
BufferAccess(92, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7),
|
||||
EndProgram(100));
|
||||
var (plan, resources, layout) = Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
using var harness = new ImageTestHarness(vulkan);
|
||||
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
|
||||
var result = runner.CreateBuffer(ThreadCount * RowBytes);
|
||||
var registers = new uint[256];
|
||||
registers[6] = ThreadCount * RowBytes;
|
||||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
|
||||
var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes);
|
||||
for (var lane = 0u; lane < ThreadCount; lane++)
|
||||
{
|
||||
var row = actual.AsSpan((int)(lane * RowBytes));
|
||||
var large = MathF.FusedMultiplyAdd(lane, 5003.7f, -80000.3f);
|
||||
var small = MathF.FusedMultiplyAdd(lane, 3.3e-6f, -5e-5f);
|
||||
var packed = BinaryPrimitives.ReadUInt32LittleEndian(row);
|
||||
Assert.Equal(HalfTowardZero(large), packed & 0xFFFF);
|
||||
Assert.Equal(HalfTowardZero(small), packed >> 16);
|
||||
var special = BinaryPrimitives.ReadUInt32LittleEndian(row[4..]);
|
||||
Assert.True((special & 0x7C00) == 0x7C00 && (special & 0x3FF) != 0, $"lane {lane}: 0x{special:X8}");
|
||||
Assert.Equal(0x7C00u, special >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint HalfTowardZero(float value)
|
||||
{
|
||||
var sign = value < 0 || (value == 0 && float.IsNegative(value)) ? 0x8000u : 0u;
|
||||
var magnitude = MathF.Abs(value);
|
||||
if (magnitude >= 65504f) return sign | 0x7BFF;
|
||||
if (magnitude < 6.10351562e-05f) return sign | (uint)Math.Floor(magnitude * 16777216.0);
|
||||
var bits = BitConverter.SingleToUInt32Bits(magnitude);
|
||||
return sign | ((((bits >> 23) - 127 + 15) << 10) | ((bits >> 13) & 0x3FF));
|
||||
}
|
||||
|
||||
private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,8 +128,9 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
|
|||
{
|
||||
return Create();
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[TEST][VULKAN] Device setup failed: {exception}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -215,22 +216,33 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
|
|||
var appInfo = new ApplicationInfo { SType = StructureType.ApplicationInfo, ApiVersion = apiVersion };
|
||||
var validation = Environment.GetEnvironmentVariable(ValidationVariable) == "1";
|
||||
var layers = validation ? SilkMarshal.StringArrayToPtr(new[] { "VK_LAYER_KHRONOS_validation" }) : 0;
|
||||
var extensions = validation ? SilkMarshal.StringArrayToPtr(new[] { ExtDebugUtils.ExtensionName }) : 0;
|
||||
uint instanceExtensionCount = 0;
|
||||
vk.EnumerateInstanceExtensionProperties((byte*)null, &instanceExtensionCount, null);
|
||||
var instanceExtensions = new ExtensionProperties[instanceExtensionCount];
|
||||
fixed (ExtensionProperties* pointer = instanceExtensions)
|
||||
vk.EnumerateInstanceExtensionProperties((byte*)null, &instanceExtensionCount, pointer);
|
||||
var portable = instanceExtensions.Any(extension =>
|
||||
SilkMarshal.PtrToString((nint)extension.ExtensionName) == "VK_KHR_portability_enumeration");
|
||||
var instanceNames = new List<string>();
|
||||
if (validation) instanceNames.Add(ExtDebugUtils.ExtensionName);
|
||||
if (portable) instanceNames.Add("VK_KHR_portability_enumeration");
|
||||
var extensions = instanceNames.Count != 0 ? SilkMarshal.StringArrayToPtr(instanceNames.ToArray()) : 0;
|
||||
var instanceInfo = new InstanceCreateInfo
|
||||
{
|
||||
SType = StructureType.InstanceCreateInfo,
|
||||
Flags = portable ? InstanceCreateFlags.EnumeratePortabilityBitKhr : 0,
|
||||
PApplicationInfo = &appInfo,
|
||||
EnabledLayerCount = validation ? 1u : 0u,
|
||||
PpEnabledLayerNames = (byte**)layers,
|
||||
EnabledExtensionCount = validation ? 1u : 0u,
|
||||
EnabledExtensionCount = (uint)instanceNames.Count,
|
||||
PpEnabledExtensionNames = (byte**)extensions,
|
||||
};
|
||||
var created = vk.CreateInstance(&instanceInfo, null, out var instance);
|
||||
if (validation)
|
||||
{
|
||||
SilkMarshal.Free(layers);
|
||||
SilkMarshal.Free(extensions);
|
||||
}
|
||||
if (extensions != 0) SilkMarshal.Free(extensions);
|
||||
|
||||
if (created != Result.Success)
|
||||
{
|
||||
|
|
@ -368,6 +380,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
|
|||
vulkan13Features.PNext = &barycentricFeatures;
|
||||
}
|
||||
var extensionNames = new List<string>();
|
||||
if (HasDeviceExtensions(vk, physical, ["VK_KHR_portability_subset"])) extensionNames.Add("VK_KHR_portability_subset");
|
||||
if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames);
|
||||
if (barycentric) extensionNames.Add(barycentricExtension);
|
||||
const string fillRectangleExtension = "VK_NV_fill_rectangle";
|
||||
|
|
|
|||
Loading…
Reference in New Issue