mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 15:09:42 +08:00
Astro Bot stack: VEH/TBB, title clear, swapchain fallback, Psml MFSR (#528)
* Cpu/Kernel: harden VEH trampoline and keep TBB on native workers Route FastFail/CLR/stack-overflow around managed VEH, serialize managed entry with a recursive spinlock, require native workers for tbb_thead, and abandon pthread mutexes when a guest thread is torn down by worker abort so splash waiters are not left holding locks forever. * Cpu: soft-fail TBB native worker storms and cap concurrent Runs Throwing on worker/prologue faults killed the process mid tbb_thead burst (FailFast 0xC0000409). Soft-return 0x80020012, limit in-flight native Runs (default 2), and keep prewarm small so back-to-back boots do not need an artificial settle delay. * Agc/VideoOut: poison-only empty-SRT reject and clear procedural ES/PS Skip QueueSubmit only when Address-0 image slots remain; run the Astro title clear pair via CmdClearColorImage so the pass executes without descriptors that lose the device. * VideoOut: recreate swapchain with fallback extent on 0x0 surface Minimized Win32 surfaces report 0x0 / MaxImageExtent=0; deferring recreate forever left an OutOfDate swapchain with no presents. Clamp to last/default size and recreate instead of early-return. * Psml: stub MFSR init/shared/context and dispatch packet size Astro Bot asserts in GfxRenderStagePSSR when scePsmlMfsrInit is unresolved (Mfsr initialized failed). Soft HLE for the MFSR shared-resource and 800M3_2 context path plus dispatch packet size lets boot pass splash to first frame without claiming real upscaling. * Psml: stub MFSR GetDispatchMfsrPacket900 for logo PSSR Astro StartLevel ps_logo asserted GfxRenderStagePSSR.cpp:266 when GetDispatchMfsrPacket900 (RUNLFro+qok) was unresolved. Return SCE_OK from SizeInDwords so the 900 fill runs, soft-clear the guest packet buffer, and register 1000/1100 siblings for the same ABI.
This commit is contained in:
+418
-106
@@ -203,6 +203,8 @@ public static partial class AgcExports
|
||||
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();
|
||||
private static readonly HashSet<ulong> _tracedEmptySrtDrawRejects = new();
|
||||
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedFixedFullscreenClears = new();
|
||||
private static readonly HashSet<(ulong Address, uint X, uint Y, uint Z)>
|
||||
_tracedDispatchArguments = new();
|
||||
private static readonly HashSet<(ulong Address, uint Initiator, string Reason)>
|
||||
@@ -439,7 +441,12 @@ public static partial class AgcExports
|
||||
uint RawBlendControl,
|
||||
uint RawColorInfo,
|
||||
IReadOnlyList<uint> PixelInitialScalars,
|
||||
IReadOnlyList<uint> VertexInitialScalars);
|
||||
IReadOnlyList<uint> VertexInitialScalars,
|
||||
bool IsFullscreenColorClear = false,
|
||||
float ClearRed = 0f,
|
||||
float ClearGreen = 0f,
|
||||
float ClearBlue = 0f,
|
||||
float ClearAlpha = 1f);
|
||||
|
||||
private sealed record TranslatedImageBinding(
|
||||
TextureDescriptor Descriptor,
|
||||
@@ -5863,21 +5870,34 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
sharedTextures,
|
||||
sharedGlobalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
translatedDraw.GuestTargets,
|
||||
translatedDraw.VertexShader,
|
||||
translatedDraw.VertexCount,
|
||||
translatedDraw.InstanceCount,
|
||||
translatedDraw.PrimitiveType,
|
||||
translatedDraw.IndexBuffer,
|
||||
sharedVertexBuffers,
|
||||
translatedDraw.RenderState,
|
||||
translatedDraw.DepthTarget,
|
||||
translatedDraw.PixelShaderAddress);
|
||||
if (translatedDraw.IsFullscreenColorClear)
|
||||
{
|
||||
VulkanVideoPresenter.SubmitOffscreenColorClear(
|
||||
translatedDraw.GuestTargets,
|
||||
translatedDraw.ClearRed,
|
||||
translatedDraw.ClearGreen,
|
||||
translatedDraw.ClearBlue,
|
||||
translatedDraw.ClearAlpha,
|
||||
translatedDraw.PixelShaderAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||
translatedDraw.PixelShader,
|
||||
sharedTextures,
|
||||
sharedGlobalMemoryBuffers,
|
||||
translatedDraw.AttributeCount,
|
||||
translatedDraw.GuestTargets,
|
||||
translatedDraw.VertexShader,
|
||||
translatedDraw.VertexCount,
|
||||
translatedDraw.InstanceCount,
|
||||
translatedDraw.PrimitiveType,
|
||||
translatedDraw.IndexBuffer,
|
||||
sharedVertexBuffers,
|
||||
translatedDraw.RenderState,
|
||||
translatedDraw.DepthTarget,
|
||||
translatedDraw.PixelShaderAddress);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -6316,6 +6336,89 @@ public static partial class AgcExports
|
||||
return false;
|
||||
}
|
||||
|
||||
// Empty SRT/EUD is fine for clears/passthroughs that bind nothing
|
||||
// (Astro title PS 0x808E88000 is a procedural fullscreen clear).
|
||||
// Reject only when evaluation produced image/global slots that
|
||||
// collapsed to Address-0 — that layout mismatches SPIR-V and loses
|
||||
// the device on QueueSubmit.
|
||||
if (pixelState.Metadata is
|
||||
{
|
||||
ShaderResourceTableSizeDwords: 0,
|
||||
ExtendedUserDataSizeDwords: 0,
|
||||
} ||
|
||||
Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(
|
||||
pixelShaderAddress))
|
||||
{
|
||||
var hasAnyImageSlot = pixelEvaluation.ImageBindings.Count > 0;
|
||||
var hasUsablePixelImage = false;
|
||||
foreach (var binding in pixelEvaluation.ImageBindings)
|
||||
{
|
||||
if (TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture) &&
|
||||
texture.Address != 0)
|
||||
{
|
||||
hasUsablePixelImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var hasUsablePixelGlobal = pixelEvaluation.GlobalMemoryBindings.Any(
|
||||
static binding => binding.BaseAddress != 0);
|
||||
var hasPoisonImageSlots = hasAnyImageSlot && !hasUsablePixelImage;
|
||||
if (hasPoisonImageSlots && !hasUsablePixelGlobal)
|
||||
{
|
||||
error = Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(
|
||||
pixelShaderAddress)
|
||||
? "empty-srt-scalar-pointer-fallback"
|
||||
: "empty-srt-no-usable-resources";
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
if (_tracedEmptySrtDrawRejects.Add(pixelShaderAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.draw_reject ps=0x{pixelShaderAddress:X16} " +
|
||||
$"es=0x{exportShaderAddress:X16} reason={error}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.draw_reject_state ps=0x{pixelShaderAddress:X16} " +
|
||||
$"header=0x{pixelShaderHeader:X16} " +
|
||||
Gen5ShaderTranslator.DescribeState(pixelState));
|
||||
var shDump = new List<string>(16);
|
||||
for (uint reg = 0x8; reg <= 0x1C; reg++)
|
||||
{
|
||||
if (state.ShRegisters.TryGetValue(reg, out var value))
|
||||
{
|
||||
shDump.Add($"0x{reg:X}={value:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.draw_reject_sh ps=0x{pixelShaderAddress:X16} " +
|
||||
$"[{string.Join(',', shDump)}]");
|
||||
var bindingIndex = 0;
|
||||
foreach (var binding in pixelEvaluation.ImageBindings)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.draw_reject_binding ps=0x{pixelShaderAddress:X16} " +
|
||||
$"[{bindingIndex++}] pc=0x{binding.Pc:X} op={binding.Opcode} " +
|
||||
$"resource={FormatShaderDwords(binding.ResourceDescriptor)} " +
|
||||
$"sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
|
||||
}
|
||||
|
||||
foreach (var binding in pixelEvaluation.GlobalMemoryBindings)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.draw_reject_global ps=0x{pixelShaderAddress:X16} " +
|
||||
$"s{binding.ScalarAddress} base=0x{binding.BaseAddress:X16} " +
|
||||
$"bytes={binding.DataLength}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pixelShaderAddress == 0x0000000500781200 &&
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS") == "1")
|
||||
{
|
||||
@@ -6421,107 +6524,170 @@ public static partial class AgcExports
|
||||
? guestGlobalBuffers
|
||||
: guestGlobalBuffers + 2;
|
||||
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
|
||||
var usedFixedFullscreenClear = false;
|
||||
(float Red, float Green, float Blue, float Alpha) fullscreenClearColor = default;
|
||||
|
||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||
{
|
||||
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
||||
for (var location = 0; location < renderTargets.Length; location++)
|
||||
{
|
||||
pixelOutputs[location] = new Gen5PixelOutputBinding(
|
||||
renderTargets[location].Slot,
|
||||
(uint)location,
|
||||
renderTargetOutputKinds[location]);
|
||||
}
|
||||
|
||||
if (!GuestGpu.Current.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
pixelOutputs,
|
||||
out var pixelShader,
|
||||
out error,
|
||||
globalBufferBase: 0,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers,
|
||||
pixelInputEnable: psInputEna,
|
||||
pixelInputAddress: psInputAddr,
|
||||
storageBufferOffsetAlignment:
|
||||
_storageBufferOffsetAlignment) ||
|
||||
!GuestGpu.Current.TryCompileVertexShader(
|
||||
if (IsProceduralFullscreenClearPair(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
out error,
|
||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1,
|
||||
requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState),
|
||||
storageBufferOffsetAlignment:
|
||||
_storageBufferOffsetAlignment))
|
||||
pixelState,
|
||||
pixelEvaluation))
|
||||
{
|
||||
// Title ES/PS clear (0x808E88D00/0x808E88000): empty SRT/EUD.
|
||||
// Gen5→SPIR-V and even fixed fragment pipelines have lost the
|
||||
// device on the 2432x1368 offscreen submit. Apply the solid
|
||||
// clear via CmdClearColorImage so the pass still runs without
|
||||
// Address-0 descriptors or a graphics pipeline.
|
||||
usedFixedFullscreenClear = true;
|
||||
fullscreenClearColor = DecodeSolidClearColor(pixelEvaluation);
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
if (_tracedFixedFullscreenClears.Add(
|
||||
(exportShaderAddress, pixelShaderAddress)))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.shader_color_clear " +
|
||||
$"es=0x{exportShaderAddress:X16} " +
|
||||
$"ps=0x{pixelShaderAddress:X16} " +
|
||||
$"rgba=({fullscreenClearColor.Red:0.###}," +
|
||||
$"{fullscreenClearColor.Green:0.###}," +
|
||||
$"{fullscreenClearColor.Blue:0.###}," +
|
||||
$"{fullscreenClearColor.Alpha:0.###})");
|
||||
}
|
||||
}
|
||||
|
||||
compiled = (
|
||||
GuestGpu.Current.GetDepthOnlyFragmentShader(),
|
||||
GuestGpu.Current.GetDepthOnlyFragmentShader());
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
||||
for (var location = 0; location < renderTargets.Length; location++)
|
||||
{
|
||||
pixelOutputs[location] = new Gen5PixelOutputBinding(
|
||||
renderTargets[location].Slot,
|
||||
(uint)location,
|
||||
renderTargetOutputKinds[location]);
|
||||
}
|
||||
|
||||
if (!GuestGpu.Current.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
pixelOutputs,
|
||||
out var pixelShader,
|
||||
out error,
|
||||
globalBufferBase: 0,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers,
|
||||
pixelInputEnable: psInputEna,
|
||||
pixelInputAddress: psInputAddr,
|
||||
storageBufferOffsetAlignment:
|
||||
_storageBufferOffsetAlignment) ||
|
||||
!GuestGpu.Current.TryCompileVertexShader(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
out error,
|
||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1,
|
||||
requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState),
|
||||
storageBufferOffsetAlignment:
|
||||
_storageBufferOffsetAlignment))
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
return false;
|
||||
}
|
||||
|
||||
compiled = (vertexShader!, pixelShader!);
|
||||
DumpCompiledShader(
|
||||
"vs",
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
compiled.Vertex,
|
||||
exportState.Program);
|
||||
DumpCompiledShader(
|
||||
"ps",
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
compiled.Pixel,
|
||||
pixelState.Program);
|
||||
GuestGpu.Current.CountShaderCompilation();
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
}
|
||||
}
|
||||
else if (IsCachedFixedFullscreenClearPair(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
pixelState,
|
||||
pixelEvaluation))
|
||||
{
|
||||
usedFixedFullscreenClear = true;
|
||||
fullscreenClearColor = DecodeSolidClearColor(pixelEvaluation);
|
||||
}
|
||||
|
||||
var useFixedFullscreenClear = usedFixedFullscreenClear;
|
||||
|
||||
List<TranslatedImageBinding> textures;
|
||||
Gen5GlobalMemoryBinding[] globalMemoryBindings;
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs;
|
||||
if (useFixedFullscreenClear)
|
||||
{
|
||||
textures = [];
|
||||
globalMemoryBindings = [];
|
||||
vertexInputs = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
var imageBindings = pixelEvaluation.ImageBindings
|
||||
.Concat(exportEvaluation.ImageBindings)
|
||||
.ToArray();
|
||||
textures = new List<TranslatedImageBinding>(
|
||||
pixelEvaluation.ImageBindings.Count +
|
||||
exportEvaluation.ImageBindings.Count);
|
||||
if (!TryAppendTranslatedImageBindings(
|
||||
pixelEvaluation.ImageBindings,
|
||||
imageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error) ||
|
||||
!TryAppendTranslatedImageBindings(
|
||||
exportEvaluation.ImageBindings,
|
||||
imageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error))
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
return false;
|
||||
}
|
||||
|
||||
compiled = (vertexShader!, pixelShader!);
|
||||
DumpCompiledShader(
|
||||
"vs",
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
compiled.Vertex,
|
||||
exportState.Program);
|
||||
DumpCompiledShader(
|
||||
"ps",
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
compiled.Pixel,
|
||||
pixelState.Program);
|
||||
GuestGpu.Current.CountShaderCompilation();
|
||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
||||
globalMemoryBindings = new Gen5GlobalMemoryBinding[
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count];
|
||||
for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++)
|
||||
{
|
||||
globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index];
|
||||
}
|
||||
for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++)
|
||||
{
|
||||
globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] =
|
||||
exportEvaluation.GlobalMemoryBindings[index];
|
||||
}
|
||||
|
||||
vertexInputs = exportEvaluation.VertexInputs ?? [];
|
||||
}
|
||||
|
||||
var imageBindings = pixelEvaluation.ImageBindings
|
||||
.Concat(exportEvaluation.ImageBindings)
|
||||
.ToArray();
|
||||
var textures = new List<TranslatedImageBinding>(
|
||||
pixelEvaluation.ImageBindings.Count +
|
||||
exportEvaluation.ImageBindings.Count);
|
||||
if (!TryAppendTranslatedImageBindings(
|
||||
pixelEvaluation.ImageBindings,
|
||||
imageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error) ||
|
||||
!TryAppendTranslatedImageBindings(
|
||||
exportEvaluation.ImageBindings,
|
||||
imageBindings,
|
||||
textures,
|
||||
pixelShaderAddress,
|
||||
exportShaderAddress,
|
||||
out error))
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
return false;
|
||||
}
|
||||
|
||||
var globalMemoryBindings = new Gen5GlobalMemoryBinding[
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count];
|
||||
for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++)
|
||||
{
|
||||
globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index];
|
||||
}
|
||||
for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++)
|
||||
{
|
||||
globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] =
|
||||
exportEvaluation.GlobalMemoryBindings[index];
|
||||
}
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
||||
for (var index = 0; index < renderTargets.Length; index++)
|
||||
@@ -6570,7 +6736,12 @@ public static partial class AgcExports
|
||||
? rawInfo
|
||||
: 0,
|
||||
pixelEvaluation.InitialScalarRegisters,
|
||||
exportEvaluation.InitialScalarRegisters);
|
||||
exportEvaluation.InitialScalarRegisters,
|
||||
useFixedFullscreenClear,
|
||||
fullscreenClearColor.Red,
|
||||
fullscreenClearColor.Green,
|
||||
fullscreenClearColor.Blue,
|
||||
fullscreenClearColor.Alpha);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6686,6 +6857,119 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCachedFixedFullscreenClearPair(
|
||||
Gen5ShaderState exportState,
|
||||
Gen5ShaderEvaluation exportEvaluation,
|
||||
Gen5ShaderState pixelState,
|
||||
Gen5ShaderEvaluation pixelEvaluation) =>
|
||||
IsProceduralFullscreenClearPair(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
pixelState,
|
||||
pixelEvaluation);
|
||||
|
||||
private static bool IsProceduralFullscreenClearPair(
|
||||
Gen5ShaderState exportState,
|
||||
Gen5ShaderEvaluation exportEvaluation,
|
||||
Gen5ShaderState pixelState,
|
||||
Gen5ShaderEvaluation pixelEvaluation)
|
||||
{
|
||||
if ((exportEvaluation.VertexInputs?.Count ?? 0) != 0 ||
|
||||
exportEvaluation.ImageBindings.Count != 0 ||
|
||||
pixelEvaluation.ImageBindings.Count != 0 ||
|
||||
exportEvaluation.GlobalMemoryBindings.Count != 0 ||
|
||||
pixelEvaluation.GlobalMemoryBindings.Count != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HasExportTarget(exportState, target: 12) ||
|
||||
!HasExportTarget(pixelState, target: 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pixelState.Program.Instructions.Count is 0 or > 8 ||
|
||||
exportState.Program.Instructions.Count is 0 or > 48)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return pixelState.Program.Instructions.All(IsBenignClearPixelInstruction) &&
|
||||
exportState.Program.Instructions.All(IsBenignProceduralVertexInstruction);
|
||||
}
|
||||
|
||||
private static bool HasExportTarget(Gen5ShaderState state, uint target) =>
|
||||
state.Program.Instructions.Any(instruction =>
|
||||
instruction.Control is Gen5ExportControl export &&
|
||||
export.Target == target);
|
||||
|
||||
private static bool IsBenignClearPixelInstruction(Gen5ShaderInstruction instruction) =>
|
||||
instruction.Opcode is
|
||||
"SNop" or
|
||||
"SWaitcnt" or
|
||||
"SInstPrefetch" or
|
||||
"SEndpgm" or
|
||||
"VMovB32" ||
|
||||
instruction.Control is Gen5ExportControl { Target: 0 };
|
||||
|
||||
private static bool IsBenignProceduralVertexInstruction(Gen5ShaderInstruction instruction)
|
||||
{
|
||||
if (instruction.Control is Gen5BufferMemoryControl or
|
||||
Gen5ImageControl or
|
||||
Gen5GlobalMemoryControl or
|
||||
Gen5ScalarMemoryControl)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Control is Gen5ExportControl export)
|
||||
{
|
||||
// Position (12) plus ignored NGG/param exports.
|
||||
return export.Target is 12 or (>= 13 and < 32) or 20;
|
||||
}
|
||||
|
||||
return instruction.Opcode is
|
||||
"SNop" or
|
||||
"SWaitcnt" or
|
||||
"SInstPrefetch" or
|
||||
"SEndpgm" or
|
||||
"SSendmsg" or
|
||||
"VMovB32" or
|
||||
"VAndB32" or
|
||||
"VAddI32" or
|
||||
"VLshlrevB32" or
|
||||
"VCvtF32I32" or
|
||||
"VCvtF32U32" ||
|
||||
instruction.Encoding is
|
||||
Gen5ShaderEncoding.Sop1 or
|
||||
Gen5ShaderEncoding.Sop2 or
|
||||
Gen5ShaderEncoding.Sopc or
|
||||
Gen5ShaderEncoding.Sopk or
|
||||
Gen5ShaderEncoding.Sopp;
|
||||
}
|
||||
|
||||
private static (float Red, float Green, float Blue, float Alpha) DecodeSolidClearColor(
|
||||
Gen5ShaderEvaluation pixelEvaluation)
|
||||
{
|
||||
// Default opaque white; guest clear shaders often mov a 1.0 literal into v0.
|
||||
float red = 1f, green = 1f, blue = 1f, alpha = 1f;
|
||||
if (pixelEvaluation.InitialScalarRegisters.Count > 0)
|
||||
{
|
||||
var bits = pixelEvaluation.InitialScalarRegisters[0];
|
||||
if (bits != 0)
|
||||
{
|
||||
red = green = blue = alpha = BitConverter.UInt32BitsToSingle(bits);
|
||||
if (!float.IsFinite(red) || red < 0f || red > 4f)
|
||||
{
|
||||
red = green = blue = alpha = 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (red, green, blue, alpha);
|
||||
}
|
||||
|
||||
private static readonly bool _fillClearHack = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_FILL_CLEAR"),
|
||||
"1",
|
||||
@@ -9190,7 +9474,35 @@ public static partial class AgcExports
|
||||
var gpuDispatch = false;
|
||||
var evaluationHandledByCpu = false;
|
||||
var computeError = string.Empty;
|
||||
if (!hasStorageBinding &&
|
||||
// Empty SRT/EUD with a recorded null-base scalar pointer fallback
|
||||
// produces Address-0 storage that can lose the Vulkan device on submit.
|
||||
var emptyResourceTables =
|
||||
shaderState.Metadata is
|
||||
{
|
||||
ShaderResourceTableSizeDwords: 0,
|
||||
ExtendedUserDataSizeDwords: 0,
|
||||
};
|
||||
if (emptyResourceTables &&
|
||||
(Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(shaderAddress) ||
|
||||
(translatedBindings.All(static binding => binding.Descriptor.Address == 0) &&
|
||||
!evaluation.GlobalMemoryBindings.Any(static binding => binding.BaseAddress != 0))))
|
||||
{
|
||||
computeError = Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(shaderAddress)
|
||||
? "empty-srt-scalar-pointer-fallback"
|
||||
: "empty-srt-no-usable-resources";
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
if (_tracedComputeShaders.Add(shaderAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.compute_reject cs=0x{shaderAddress:X16} " +
|
||||
$"source={(dispatch.IsIndirect ? "indirect" : "direct")} " +
|
||||
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
||||
$"reason={computeError}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!hasStorageBinding &&
|
||||
writesGlobalMemory &&
|
||||
TrySubmitMaskedDwordCopyKernel(
|
||||
ctx,
|
||||
|
||||
@@ -153,6 +153,64 @@ public static class KernelPthreadCompatExports
|
||||
static KernelPthreadCompatExports()
|
||||
{
|
||||
RunSynchronizationSelfChecks();
|
||||
GuestThreadExecution.GuestThreadAbandoned += AbandonMutexesOwnedByThread;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-release mutexes still owned by a guest thread that is being torn
|
||||
/// down without a clean unlock (TBB worker_abort, abrupt exit). Otherwise
|
||||
/// waiters can spin forever and block splash→first GPU submit.
|
||||
/// </summary>
|
||||
public static int AbandonMutexesOwnedByThread(ulong threadId, string reason)
|
||||
{
|
||||
if (threadId == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var released = 0;
|
||||
var wakeKeys = new List<string>();
|
||||
foreach (var pair in _mutexStates)
|
||||
{
|
||||
var state = pair.Value;
|
||||
string? wakeKey = null;
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId != threadId || state.RecursionCount <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.OwnerThreadId = 0;
|
||||
state.RecursionCount = 0;
|
||||
wakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||
? state.Waiters.First.Value.WakeKey
|
||||
: null;
|
||||
Monitor.PulseAll(state);
|
||||
released++;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] pthread_mutex_abandon mutex=0x{pair.Key:X16} " +
|
||||
$"owner={KernelPthreadState.DescribeThreadHandle(threadId)} " +
|
||||
$"reason={reason} waiters={state.Waiters.Count}");
|
||||
}
|
||||
|
||||
if (wakeKey is not null)
|
||||
{
|
||||
wakeKeys.Add(wakeKey);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var wakeKey in wakeKeys)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(wakeKey, 1);
|
||||
}
|
||||
|
||||
if (released > 0)
|
||||
{
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
return released;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -27,8 +27,12 @@ internal static class KernelPthreadState
|
||||
internal static ulong GetCurrentThreadHandle()
|
||||
{
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _))
|
||||
// Prefer the bound guest handle even when it is not yet in Threads.
|
||||
// Falling through to a synthetic ThreadStatic handle while a guest
|
||||
// thread is bound causes mutex owner mismatches (unlock PERM → hang).
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
EnsureGuestThreadIdentity(guestThreadHandle);
|
||||
return guestThreadHandle;
|
||||
}
|
||||
|
||||
@@ -39,15 +43,27 @@ internal static class KernelPthreadState
|
||||
internal static ulong GetCurrentThreadUniqueId()
|
||||
{
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity))
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
return identity.UniqueId;
|
||||
return EnsureGuestThreadIdentity(guestThreadHandle).UniqueId;
|
||||
}
|
||||
|
||||
EnsureCurrentThreadRegistered();
|
||||
return _currentThreadUniqueId;
|
||||
}
|
||||
|
||||
internal static string DescribeThreadHandle(ulong threadHandle)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
return TryGetThreadIdentity(threadHandle, out var identity)
|
||||
? $"0x{threadHandle:X16}('{identity.Name}')"
|
||||
: $"0x{threadHandle:X16}";
|
||||
}
|
||||
|
||||
internal static ulong CreateThreadHandle(string name)
|
||||
{
|
||||
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
||||
@@ -59,6 +75,18 @@ internal static class KernelPthreadState
|
||||
return Threads.TryGetValue(threadHandle, out identity);
|
||||
}
|
||||
|
||||
private static ThreadIdentity EnsureGuestThreadIdentity(ulong guestThreadHandle)
|
||||
{
|
||||
if (Threads.TryGetValue(guestThreadHandle, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
||||
var identity = new ThreadIdentity(uniqueId, $"Guest-0x{guestThreadHandle:X}");
|
||||
return Threads.GetOrAdd(guestThreadHandle, identity);
|
||||
}
|
||||
|
||||
private static void EnsureCurrentThreadRegistered()
|
||||
{
|
||||
if (_currentThreadHandle != 0)
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Psml;
|
||||
|
||||
public static class PsmlExports
|
||||
{
|
||||
// Empirically for Astro Bot (PPSA21567):
|
||||
// [0] must be 0x80 (sizeThis / r9); any other value в†’ Allocate length=0
|
||||
// AllocateMainDirectMemory(length=[0]*[16], alignment=[8], type=0xC)
|
||||
// So put desired byte size in [8] (becomes alignment) and page size in [16].
|
||||
private const ulong RequirementStructSize = 0x80;
|
||||
private const ulong SharedResourcesPageSize = 0x10000;
|
||||
private const ulong SharedResourcesBufferSizeBytes = 0x2000000;
|
||||
private const ulong SharedResourcesContextSizeBytes = 0x100000;
|
||||
private const ulong ContextBufferSizeBytes = 0x800000;
|
||||
private const ulong ContextBufferAuxSizeBytes = 0x100000;
|
||||
|
||||
private static int _mfsrInitialized;
|
||||
private static readonly Lock SharedResourcesGate = new();
|
||||
private static readonly Dictionary<ulong, SharedResourcesState> SharedResourcesByDescriptor = new();
|
||||
private static readonly Lock ContextGate = new();
|
||||
private static readonly Dictionary<ulong, ContextState> ContextsByAddress = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3WVD91e12ZQ",
|
||||
ExportName = "scePsmlMfsrInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrInit(CpuContext ctx)
|
||||
{
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
var arg1 = ctx[CpuRegister.Rsi];
|
||||
var arg2 = ctx[CpuRegister.Rdx];
|
||||
Interlocked.Exchange(ref _mfsrInitialized, 1);
|
||||
TracePsml($"mfsr_init arg0=0x{arg0:X} arg1=0x{arg1:X} arg2=0x{arg2:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+2KpvixvL6E",
|
||||
ExportName = "scePsmlMfsrGetSharedResourcesInitRequirement",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetSharedResourcesInitRequirement(CpuContext ctx)
|
||||
{
|
||||
var bufferRequirementAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var flags = ctx[CpuRegister.Rdx];
|
||||
var configAddress = ctx[CpuRegister.Rcx];
|
||||
if (bufferRequirementAddress == 0 || contextRequirementAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, SharedResourcesBufferSizeBytes) ||
|
||||
!WriteMemoryRequirement(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_get_shared_resources_init_requirement buf=0x{bufferRequirementAddress:X16} " +
|
||||
$"ctx=0x{contextRequirementAddress:X16} flags=0x{flags:X} config=0x{configAddress:X16} " +
|
||||
$"buf_size=0x{SharedResourcesBufferSizeBytes:X} ctx_size=0x{SharedResourcesContextSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eWoKNeB6V-k",
|
||||
ExportName = "scePsmlMfsrCreateSharedResources",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrCreateSharedResources(CpuContext ctx)
|
||||
{
|
||||
var descriptorAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var directMemoryAddress = ctx[CpuRegister.Rdx];
|
||||
if (descriptorAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var contextSizeBytes = ReadRequirementSize(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes);
|
||||
var state = new SharedResourcesState(
|
||||
DescriptorAddress: descriptorAddress,
|
||||
DirectMemoryAddress: directMemoryAddress,
|
||||
BufferSizeBytes: SharedResourcesBufferSizeBytes,
|
||||
ContextSizeBytes: contextSizeBytes,
|
||||
PageSizeBytes: SharedResourcesPageSize);
|
||||
|
||||
if (!WriteSharedResourcesDescriptor(ctx, state))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (SharedResourcesGate)
|
||||
{
|
||||
SharedResourcesByDescriptor[descriptorAddress] = state;
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_create_shared_resources desc=0x{descriptorAddress:X16} req=0x{contextRequirementAddress:X16} " +
|
||||
$"direct=0x{directMemoryAddress:X16} buf_size=0x{state.BufferSizeBytes:X} " +
|
||||
$"ctx_size=0x{state.ContextSizeBytes:X} page=0x{state.PageSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ArakEpzsZo0",
|
||||
ExportName = "scePsmlMfsrGetContextBufferRequirement800M3_2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetContextBufferRequirement800M3_2(CpuContext ctx)
|
||||
{
|
||||
var bufferRequirementAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var directMemoryAddress = ctx[CpuRegister.Rdx];
|
||||
if (bufferRequirementAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, ContextBufferSizeBytes) ||
|
||||
!WriteMemoryRequirement(ctx, contextRequirementAddress, ContextBufferAuxSizeBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_get_context_buffer_requirement_800m3_2 buf=0x{bufferRequirementAddress:X16} " +
|
||||
$"ctx=0x{contextRequirementAddress:X16} direct=0x{directMemoryAddress:X16} " +
|
||||
$"buf_size=0x{ContextBufferSizeBytes:X} aux_size=0x{ContextBufferAuxSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gxv3i+MTEzU",
|
||||
ExportName = "scePsmlMfsrCreateContext800M3_2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrCreateContext800M3_2(CpuContext ctx)
|
||||
{
|
||||
var contextAddress = ctx[CpuRegister.Rdi];
|
||||
var requirementAddress = ctx[CpuRegister.Rsi];
|
||||
var structSize = ctx[CpuRegister.Rdx];
|
||||
var sharedDirectMemory = ctx[CpuRegister.Rcx];
|
||||
var pageSize = ctx[CpuRegister.R8];
|
||||
if (contextAddress == 0 || sharedDirectMemory == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var sharedState = TryFindSharedResources(sharedDirectMemory, contextAddress);
|
||||
var effectiveStructSize = structSize != 0 ? structSize : RequirementStructSize;
|
||||
var effectivePageSize = pageSize != 0 ? pageSize : SharedResourcesPageSize;
|
||||
var sharedDescriptor = sharedState?.DescriptorAddress ?? contextAddress - 0x30;
|
||||
var bufferSize = sharedState?.BufferSizeBytes ?? ContextBufferSizeBytes;
|
||||
|
||||
var state = new ContextState(
|
||||
ContextAddress: contextAddress,
|
||||
SharedResourcesDescriptor: sharedDescriptor,
|
||||
DirectMemoryAddress: sharedDirectMemory,
|
||||
BufferSizeBytes: bufferSize,
|
||||
PageSizeBytes: effectivePageSize,
|
||||
StructSizeBytes: effectiveStructSize);
|
||||
|
||||
if (!WriteContextObject(ctx, state))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (ContextGate)
|
||||
{
|
||||
ContextsByAddress[contextAddress] = state;
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_create_context_800m3_2 ctx=0x{contextAddress:X16} req=0x{requirementAddress:X16} " +
|
||||
$"struct=0x{effectiveStructSize:X} direct=0x{sharedDirectMemory:X16} " +
|
||||
$"shared_desc=0x{sharedDescriptor:X16} buf_size=0x{bufferSize:X} page=0x{effectivePageSize:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool WriteMemoryRequirement(CpuContext ctx, ulong address, ulong sizeBytes)
|
||||
{
|
||||
return ctx.TryWriteUInt64(address, RequirementStructSize) &&
|
||||
ctx.TryWriteUInt64(address + 0x08, sizeBytes) &&
|
||||
ctx.TryWriteUInt64(address + 0x10, SharedResourcesPageSize);
|
||||
}
|
||||
|
||||
private static ulong ReadRequirementSize(CpuContext ctx, ulong address, ulong fallback)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address + 0x08, out var sizeBytes) || sizeBytes == 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Guest requirement structs use size @8; reject pointer-like garbage.
|
||||
return sizeBytes > 0x1000_0000UL ? fallback : sizeBytes;
|
||||
}
|
||||
|
||||
private static SharedResourcesState? TryFindSharedResources(ulong directMemoryAddress, ulong contextAddress)
|
||||
{
|
||||
lock (SharedResourcesGate)
|
||||
{
|
||||
foreach (var state in SharedResourcesByDescriptor.Values)
|
||||
{
|
||||
if (state.DirectMemoryAddress == directMemoryAddress)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
var inferredDescriptor = contextAddress >= 0x30 ? contextAddress - 0x30 : 0;
|
||||
if (inferredDescriptor != 0 &&
|
||||
SharedResourcesByDescriptor.TryGetValue(inferredDescriptor, out var byDescriptor))
|
||||
{
|
||||
return byDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool WriteContextObject(CpuContext ctx, ContextState state)
|
||||
{
|
||||
return ctx.TryWriteUInt64(state.ContextAddress + 0x00, state.StructSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x08, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x10, state.SharedResourcesDescriptor) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x18, state.BufferSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x20, state.PageSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x28, state.ContextAddress) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x30, state.DirectMemoryAddress);
|
||||
}
|
||||
|
||||
private static bool WriteSharedResourcesDescriptor(CpuContext ctx, SharedResourcesState state)
|
||||
{
|
||||
// Stamp a compact self-describing blob so follow-up PSML calls can treat the
|
||||
// descriptor as initialized guest memory instead of an all-zero placeholder.
|
||||
return ctx.TryWriteUInt64(state.DescriptorAddress + 0x00, RequirementStructSize) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x08, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x10, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x18, state.BufferSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x20, state.ContextSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x28, state.PageSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x30, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x38, state.BufferSizeBytes + state.ContextSizeBytes);
|
||||
}
|
||||
|
||||
|
||||
// Astro logo path: SizeInDwords then GetDispatchMfsrPacket900 (RUNLFro+qok).
|
||||
// Unresolved 900 returns non-zero and trips GfxRenderStagePSSR.cpp:266.
|
||||
private const int SoftPacketSizeInDwords = 0x80;
|
||||
private const ulong SoftPacketSizeBytes = SoftPacketSizeInDwords * 4UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AHalTX9wFZY",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacketSizeInDwords",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacketSizeInDwords(CpuContext ctx)
|
||||
{
|
||||
// Logo/PSSR path: guest asserts ret == 0, then calls GetDispatchMfsrPacket900
|
||||
// with rdi=SoftPacketSizeInDwords (observed 0x80). Returning the size in rax
|
||||
// tripped :266 and skipped the 900 call (tDISP-s6). SCE_OK keeps the chain.
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
TracePsml(
|
||||
$"mfsr_get_dispatch_packet_size_dwords arg0=0x{arg0:X} " +
|
||||
$"size=0x{SoftPacketSizeInDwords:X} ret=0");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RUNLFro+qok",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket900",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket900(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "900");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "s2psNHUIdjk",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket1000",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket1000(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "1000");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "94iBp3KvIuI",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket1100",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket1100(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "1100");
|
||||
|
||||
private static int SoftGetDispatchMfsrPacket(CpuContext ctx, string variant)
|
||||
{
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
var arg1 = ctx[CpuRegister.Rsi];
|
||||
var arg2 = ctx[CpuRegister.Rdx];
|
||||
var arg3 = ctx[CpuRegister.Rcx];
|
||||
// Logo call shape (tDISP-s3): rdi=size_dwords (0x80), rsi/rdx = guest
|
||||
// packet/param buffers. Clear the first mapped buffer arg.
|
||||
var packetAddress = 0UL;
|
||||
foreach (var candidate in new[] { arg1, arg2, arg3 })
|
||||
{
|
||||
if (candidate >= 0x10000)
|
||||
{
|
||||
packetAddress = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var cleared = packetAddress != 0 && TryClearGuestBuffer(ctx, packetAddress, SoftPacketSizeBytes);
|
||||
TracePsml(
|
||||
$"mfsr_get_dispatch_packet_{variant} a0=0x{arg0:X16} a1=0x{arg1:X16} " +
|
||||
$"a2=0x{arg2:X16} a3=0x{arg3:X16} packet=0x{packetAddress:X16} cleared={cleared}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||
{
|
||||
Span<byte> zeroes = stackalloc byte[4096];
|
||||
zeroes.Clear();
|
||||
for (ulong offset = 0; offset < length;)
|
||||
{
|
||||
var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += unchecked((uint)chunkSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TracePsml(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PSML"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] psml.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SharedResourcesState(
|
||||
ulong DescriptorAddress,
|
||||
ulong DirectMemoryAddress,
|
||||
ulong BufferSizeBytes,
|
||||
ulong ContextSizeBytes,
|
||||
ulong PageSizeBytes);
|
||||
|
||||
private readonly record struct ContextState(
|
||||
ulong ContextAddress,
|
||||
ulong SharedResourcesDescriptor,
|
||||
ulong DirectMemoryAddress,
|
||||
ulong BufferSizeBytes,
|
||||
ulong PageSizeBytes,
|
||||
ulong StructSizeBytes);
|
||||
}
|
||||
@@ -54,6 +54,14 @@ internal sealed record VulkanOffscreenGuestDraw(
|
||||
bool PublishTarget,
|
||||
ulong ShaderAddress);
|
||||
|
||||
internal sealed record VulkanOffscreenColorClear(
|
||||
IReadOnlyList<GuestRenderTarget> Targets,
|
||||
float Red,
|
||||
float Green,
|
||||
float Blue,
|
||||
float Alpha,
|
||||
ulong ShaderAddress);
|
||||
|
||||
internal sealed record VulkanComputeGuestDispatch(
|
||||
ulong ShaderAddress,
|
||||
byte[] ComputeSpirv,
|
||||
@@ -1250,6 +1258,67 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a solid color clear to offscreen guest render targets without a
|
||||
/// graphics pipeline. Used for empty-SRT procedural clear draws that
|
||||
/// otherwise lose the device on QueueSubmit with Address-0 descriptors.
|
||||
/// </summary>
|
||||
internal static void SubmitOffscreenColorClear(
|
||||
IReadOnlyList<GuestRenderTarget> targets,
|
||||
float red,
|
||||
float green,
|
||||
float blue,
|
||||
float alpha,
|
||||
ulong shaderAddress = 0)
|
||||
{
|
||||
if (targets.Count == 0 ||
|
||||
targets.Count > 8 ||
|
||||
AnyRenderTargetInvalid(targets))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTarget = targets[0];
|
||||
if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Vulkan skipped MRT color clear with mismatched dimensions or aliased targets.");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var guestTextureFormat = GetGuestTextureFormat(
|
||||
target.Format,
|
||||
target.NumberType);
|
||||
if (guestTextureFormat != 0)
|
||||
{
|
||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
||||
}
|
||||
}
|
||||
|
||||
var workSequence = EnqueueGuestWorkLocked(
|
||||
new VulkanOffscreenColorClear(
|
||||
targets.ToArray(),
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
shaderAddress));
|
||||
foreach (var target in targets)
|
||||
{
|
||||
_guestImageWorkSequences[target.Address] = workSequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SubmitGuestImageWrite(ulong address, byte[] pixels)
|
||||
{
|
||||
lock (_gate)
|
||||
@@ -2874,6 +2943,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private readonly System.Collections.Concurrent.ConcurrentQueue<GuestImageResource> _pendingAliasImageDumps = new();
|
||||
private bool _deviceLost;
|
||||
private bool _deviceLostLogged;
|
||||
// Last guest work the render thread entered; included in device-lost
|
||||
// reports so QueueSubmit faults name the offending dispatch/draw.
|
||||
private string _activeGuestWorkLabel = string.Empty;
|
||||
// Survives the per-work finally clear: batched submits often flush
|
||||
// after the label is reset (queue switch / end-of-drain).
|
||||
private string _lastGuestWorkLabel = string.Empty;
|
||||
private string _lastSubmitDebugName = string.Empty;
|
||||
private int _directPresentationCount;
|
||||
private readonly Dictionary<ulong, long> _presentedGuestImageTraceCounts = new();
|
||||
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
||||
@@ -4884,9 +4960,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
var submitContext = ResolveGuestSubmitContext(resources);
|
||||
_lastSubmitDebugName = submitContext;
|
||||
var submitLabel = string.IsNullOrEmpty(submitContext)
|
||||
? "vkQueueSubmit(guest)"
|
||||
: $"vkQueueSubmit(guest) during {submitContext}";
|
||||
Check(
|
||||
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
|
||||
"vkQueueSubmit(guest)");
|
||||
submitLabel);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -5039,6 +5120,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (result == Result.ErrorDeviceLost)
|
||||
{
|
||||
_deviceLost = true;
|
||||
if (!_deviceLostLogged)
|
||||
{
|
||||
_deviceLostLogged = true;
|
||||
var work = !string.IsNullOrEmpty(_lastGuestWorkLabel)
|
||||
? $"last_work={_lastGuestWorkLabel}"
|
||||
: "work=<none>";
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
||||
$"{work} last_submit={oldest.DebugName} " +
|
||||
"vkWaitForFences(guest) failed with ErrorDeviceLost.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -10147,6 +10239,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
if (TryMarkDeviceLost(exception))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Vulkan device lost during compute " +
|
||||
$"cs=0x{work.ShaderAddress:X16} " +
|
||||
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
||||
$"textures={work.Textures.Count} " +
|
||||
$"globals={work.GlobalMemoryBuffers.Count} " +
|
||||
$"writes_global={(work.WritesGlobalMemory ? 1 : 0)} " +
|
||||
$"indirect={(work.IsIndirect ? 1 : 0)} " +
|
||||
$"spirv={work.ComputeSpirv.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10249,6 +10350,49 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
// Empty resource tables with non-trivial SPIR-V usually means the
|
||||
// SRT/EUD walk failed (scalar_pointer_fallback / srt=0). Binding
|
||||
// nothing while the module still declares descriptors is a common
|
||||
// device-loss trigger on the subsequent QueueSubmit.
|
||||
if (work.Textures.Count == 0 &&
|
||||
work.GlobalMemoryBuffers.Count == 0 &&
|
||||
work.ComputeSpirv.Length > 0)
|
||||
{
|
||||
error = "empty-resources";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Address-0 storage is host scratch for legitimate descriptors, but
|
||||
// after an empty SRT walk every binding can collapse to Address-0
|
||||
// fallbacks with no real globals — that path has lost the device
|
||||
// on Astro Bot right after the first presented frame.
|
||||
var hasUsableStorage = false;
|
||||
for (var i = 0; i < work.Textures.Count; i++)
|
||||
{
|
||||
var texture = work.Textures[i];
|
||||
if (texture.IsStorage && texture.Address != 0)
|
||||
{
|
||||
hasUsableStorage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var hasUsableGlobal = false;
|
||||
for (var i = 0; i < work.GlobalMemoryBuffers.Count; i++)
|
||||
{
|
||||
if (work.GlobalMemoryBuffers[i].BaseAddress != 0)
|
||||
{
|
||||
hasUsableGlobal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUsableStorage && !hasUsableGlobal)
|
||||
{
|
||||
error = "no-usable-resources";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
@@ -11056,6 +11200,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
transientFramebuffer = default;
|
||||
resources.DebugName =
|
||||
$"SharpEmu offscreen mrt={targets.Length} " +
|
||||
$"ps=0x{work.ShaderAddress:X16} " +
|
||||
$"first=0x{work.Targets[0].Address:X16} " +
|
||||
$"{firstTarget.Width}x{firstTarget.Height}";
|
||||
|
||||
@@ -11354,6 +11499,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
if (TryMarkDeviceLost(exception))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Vulkan device lost during offscreen " +
|
||||
$"vs=0x{work.ShaderAddress:X16} " +
|
||||
$"mrt={work.Targets.Count} " +
|
||||
$"textures={work.Draw.Textures.Count} " +
|
||||
$"vertices={work.Draw.VertexCount}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11397,6 +11548,133 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ExecuteOffscreenColorClear(VulkanOffscreenColorClear work)
|
||||
{
|
||||
if (_deviceLost || work.Targets.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _perfDrawCount);
|
||||
PerfOverlay.RecordDraw();
|
||||
|
||||
var targetFormats = new VulkanRenderTargetFormat[work.Targets.Count];
|
||||
for (var index = 0; index < targetFormats.Length; index++)
|
||||
{
|
||||
var target = work.Targets[index];
|
||||
if (!TryDecodeRenderTargetFormat(target.Format, target.NumberType, out targetFormats[index]) ||
|
||||
!SupportsColorAttachment(targetFormats[index].Format))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Vulkan skipped color clear for unsupported target " +
|
||||
$"0x{target.Address:X16} format={target.Format} number_type={target.NumberType}.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureGuestSubmissionCapacity();
|
||||
var commandBuffer = BeginBatchedGuestCommands();
|
||||
CloseOpenTranslatedRenderPass();
|
||||
var clearValue = new ClearColorValue(work.Red, work.Green, work.Blue, work.Alpha);
|
||||
|
||||
for (var index = 0; index < work.Targets.Count; index++)
|
||||
{
|
||||
var targetDescriptor = work.Targets[index];
|
||||
var image = GetOrCreateGuestImage(
|
||||
targetDescriptor,
|
||||
targetFormats[index].Format);
|
||||
if (TakeGuestImageInitialData(targetDescriptor.Address) is { } initialData &&
|
||||
!image.Initialized &&
|
||||
(ulong)initialData.Length ==
|
||||
(ulong)image.Width * image.Height * 4)
|
||||
{
|
||||
UploadGuestImageInitialData(image, initialData);
|
||||
}
|
||||
|
||||
var toTransferDst = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = image.Initialized ? AccessFlags.ShaderReadBit : 0,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = image.Initialized
|
||||
? ImageLayout.ShaderReadOnlyOptimal
|
||||
: ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image.Image,
|
||||
SubresourceRange = ColorSubresourceRange(0, image.MipLevels),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
image.Initialized
|
||||
? PipelineStageFlags.FragmentShaderBit
|
||||
: PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&toTransferDst);
|
||||
|
||||
var range = ColorSubresourceRange(0, image.MipLevels);
|
||||
_vk.CmdClearColorImage(
|
||||
commandBuffer,
|
||||
image.Image,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
&clearValue,
|
||||
1,
|
||||
&range);
|
||||
|
||||
var toShaderRead = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags.ShaderReadBit,
|
||||
OldLayout = ImageLayout.TransferDstOptimal,
|
||||
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image.Image,
|
||||
SubresourceRange = ColorSubresourceRange(0, image.MipLevels),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.TransferBit,
|
||||
PipelineStageFlags.FragmentShaderBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&toShaderRead);
|
||||
image.Initialized = true;
|
||||
|
||||
var guestTextureFormat = GetGuestTextureFormat(
|
||||
targetDescriptor.Format,
|
||||
targetDescriptor.NumberType);
|
||||
if (guestTextureFormat != 0)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages[image.Address] = guestTextureFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_traceVulkanShaderEnabled)
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.offscreen_color_clear mrt={work.Targets.Count} " +
|
||||
$"ps=0x{work.ShaderAddress:X16} " +
|
||||
$"rgba=({work.Red:0.###},{work.Green:0.###},{work.Blue:0.###},{work.Alpha:0.###})");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ExecuteGuestImageWrite(VulkanGuestImageWrite work)
|
||||
{
|
||||
@@ -12891,6 +13169,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// A host command buffer must never contain commands from
|
||||
// two independent guest queues: an ordered action fences
|
||||
// only its own queue's predecessor submissions.
|
||||
// Keep the previous work label so a device-lost on this
|
||||
// flush still names the draws that filled the batch.
|
||||
FlushBatchedGuestCommands();
|
||||
}
|
||||
|
||||
@@ -12905,6 +13185,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_enqueueAsImmediateQueueFollowup = true;
|
||||
_immediateFollowupTail = null;
|
||||
var work = pendingGuestWork.Work;
|
||||
_activeGuestWorkLabel = DescribeGuestWork(
|
||||
work,
|
||||
pendingGuestWork.Queue,
|
||||
pendingGuestWork.Sequence);
|
||||
_lastGuestWorkLabel = _activeGuestWorkLabel;
|
||||
var deferGuestWork = false;
|
||||
|
||||
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
|
||||
@@ -12934,6 +13219,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
case VulkanOffscreenGuestDraw offscreenDraw:
|
||||
ExecuteOffscreenDraw(offscreenDraw);
|
||||
break;
|
||||
case VulkanOffscreenColorClear colorClear:
|
||||
ExecuteOffscreenColorClear(colorClear);
|
||||
break;
|
||||
case VulkanComputeGuestDispatch computeDispatch:
|
||||
ExecuteComputeDispatch(computeDispatch);
|
||||
break;
|
||||
@@ -12959,6 +13247,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
_enqueueAsImmediateQueueFollowup = false;
|
||||
_immediateFollowupTail = null;
|
||||
_activeGuestWorkLabel = string.Empty;
|
||||
Volatile.Write(ref _executingGuestWorkSequence, 0);
|
||||
}
|
||||
|
||||
@@ -15929,7 +16218,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
value = value <= 1 && fallback > 1 ? fallback : value;
|
||||
minimum = Math.Max(minimum, 1u);
|
||||
maximum = Math.Max(maximum, minimum);
|
||||
// Minimized / unmapped Win32 surfaces often report MaxImageExtent=0.
|
||||
// Clamping the fallback into [1,1] would create a useless 1x1
|
||||
// swapchain; keep the last/default size instead.
|
||||
if (maximum < minimum)
|
||||
{
|
||||
maximum = Math.Max(fallback, minimum);
|
||||
}
|
||||
|
||||
return Math.Clamp(value, minimum, maximum);
|
||||
}
|
||||
|
||||
@@ -16214,15 +16510,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
: (uint)Math.Max(framebufferSize.Y, 0);
|
||||
if (surfaceWidth <= 1 || surfaceHeight <= 1)
|
||||
{
|
||||
// Minimized / unmapped window reports 0x0. Deferring forever
|
||||
// leaves an OutOfDate swapchain with no presents. ChooseExtent
|
||||
// already clamps 0 to last/default size — recreate with that.
|
||||
if (!_swapchainRecreateDeferred)
|
||||
{
|
||||
_swapchainRecreateDeferred = true;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan VideoOut deferred swapchain recreation: " +
|
||||
$"surface={surfaceWidth}x{surfaceHeight}");
|
||||
"[LOADER][INFO] Vulkan VideoOut swapchain recreate " +
|
||||
$"using fallback extent (window reported {surfaceWidth}x{surfaceHeight})");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_swapchainRecreateDeferred = false;
|
||||
@@ -16429,14 +16726,86 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (!_deviceLostLogged)
|
||||
{
|
||||
_deviceLostLogged = true;
|
||||
var work = !string.IsNullOrEmpty(_activeGuestWorkLabel)
|
||||
? $"work={_activeGuestWorkLabel}"
|
||||
: !string.IsNullOrEmpty(_lastGuestWorkLabel)
|
||||
? $"last_work={_lastGuestWorkLabel}"
|
||||
: "work=<none>";
|
||||
var submit = string.IsNullOrEmpty(_lastSubmitDebugName)
|
||||
? string.Empty
|
||||
: $" last_submit={_lastSubmitDebugName}";
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
||||
exception.Message);
|
||||
$"{work}{submit} {exception.Message}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string ResolveGuestSubmitContext(
|
||||
IReadOnlyList<TranslatedDrawResources> resources)
|
||||
{
|
||||
var workLabel = !string.IsNullOrEmpty(_activeGuestWorkLabel)
|
||||
? _activeGuestWorkLabel
|
||||
: _lastGuestWorkLabel;
|
||||
var resourceName = resources.Count > 0
|
||||
? resources[0].DebugName
|
||||
: _batchResources.Count > 0
|
||||
? _batchResources[0].DebugName
|
||||
: string.Empty;
|
||||
if (string.IsNullOrEmpty(workLabel))
|
||||
{
|
||||
return string.IsNullOrEmpty(resourceName)
|
||||
? string.Empty
|
||||
: $"batch={resourceName}";
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(resourceName)
|
||||
? workLabel
|
||||
: $"{workLabel} batch={resourceName}";
|
||||
}
|
||||
|
||||
private static string DescribeGuestWork(
|
||||
object work,
|
||||
VulkanGuestQueueIdentity queue,
|
||||
long sequence)
|
||||
{
|
||||
var queuePart =
|
||||
$"queue={queue.Name} submission={queue.SubmissionId} sequence={sequence}";
|
||||
return work switch
|
||||
{
|
||||
VulkanComputeGuestDispatch compute =>
|
||||
$"compute cs=0x{compute.ShaderAddress:X16} " +
|
||||
$"groups={compute.GroupCountX}x{compute.GroupCountY}x{compute.GroupCountZ} " +
|
||||
$"textures={compute.Textures.Count} " +
|
||||
$"globals={compute.GlobalMemoryBuffers.Count} " +
|
||||
$"writes_global={(compute.WritesGlobalMemory ? 1 : 0)} " +
|
||||
$"indirect={(compute.IsIndirect ? 1 : 0)} " +
|
||||
$"spirv={compute.ComputeSpirv.Length} {queuePart}",
|
||||
VulkanOffscreenGuestDraw draw =>
|
||||
$"offscreen vs=0x{draw.ShaderAddress:X16} " +
|
||||
$"mrt={draw.Targets.Count} " +
|
||||
$"textures={draw.Draw.Textures.Count} " +
|
||||
$"vertices={draw.Draw.VertexCount} {queuePart}",
|
||||
VulkanOffscreenColorClear clear =>
|
||||
$"offscreen_clear ps=0x{clear.ShaderAddress:X16} " +
|
||||
$"mrt={clear.Targets.Count} " +
|
||||
$"rgba=({clear.Red:0.###},{clear.Green:0.###},{clear.Blue:0.###},{clear.Alpha:0.###}) " +
|
||||
queuePart,
|
||||
VulkanGuestImageWrite imageWrite =>
|
||||
$"image_write addr=0x{imageWrite.Address:X16} {queuePart}",
|
||||
VulkanOrderedGuestAction action =>
|
||||
$"ordered_action name={action.DebugName} {queuePart}",
|
||||
VulkanOrderedGuestFlip flip =>
|
||||
$"ordered_flip version={flip.Version} " +
|
||||
$"buf={flip.DisplayBufferIndex} addr=0x{flip.Address:X16} {queuePart}",
|
||||
VulkanOrderedGuestFlipWait wait =>
|
||||
$"flip_wait version={wait.Version} " +
|
||||
$"buf={wait.DisplayBufferIndex} {queuePart}",
|
||||
_ => $"{work.GetType().Name} {queuePart}",
|
||||
};
|
||||
}
|
||||
|
||||
private static void TraceVulkanShader(string message)
|
||||
{
|
||||
if (!_traceVulkanShaderEnabled)
|
||||
|
||||
Reference in New Issue
Block a user