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
@@ -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)