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:
Mike Saito
2026-07-23 12:37:44 +03:00
committed by GitHub
parent 7a108c6f87
commit 96fde5764f
13 changed files with 2472 additions and 162 deletions
+418 -106
View File
@@ -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,