fix #424: a zero-area viewport skips the frame before any GPU work (alt-tab out of exclusive fullscreen crashed the pack-on client)

GLFW auto-iconifies an exclusive-fullscreen window on focus loss; for one
frame the window size reads 0x0 while the swapchain is still created, so
GameWindow.OnRender's PrepareFrame guard let a zero-area RenderFrameInput
through. The retail path tolerated it silently; the render-pack controller
correctly rejects a zero activation extent, which surfaced the latent frame
as an unhandled ArgumentOutOfRangeException during the owner's VM6/VM3 gate.

RenderFrameOrchestrator.Render now returns RenderFrameOutcome.ZeroArea
before BeginFrame when either dimension is <= 0 (no GPU frame, phase,
measurement, diagnostics or recovery runs) and GameWindow skips
NoteFrameClosed for it. Test: ZeroAreaViewport_SkipsTheFrameBeforeAnyGpuWork.

Verify: App hermetic lane 6,059/0 (Release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 09:49:56 +02:00
parent b017391b98
commit 38def07edb
4 changed files with 84 additions and 3 deletions

View file

@ -27,9 +27,23 @@ internal readonly record struct PrivatePresentationFrameOutcome(
bool ScreenshotCaptured);
/// <summary>The immutable result observed by post-screenshot diagnostics.</summary>
/// <param name="SkippedZeroArea">
/// True when the frame was never opened because the viewport had no area
/// (Campaign VM VM7 owner gate, 2026-08-23: alt-tabbing out of exclusive
/// fullscreen auto-iconifies the GLFW window, so for one frame the window
/// size reads 0x0 while the swapchain is still "created" and the host's
/// minimised guard has not yet armed; the zero-area frame then reached
/// <c>RenderPackActivationExtent.Validate</c> and crashed the client). No
/// GPU frame, measurement, phase, diagnostics or recovery ran for such a
/// frame, so the host must not note it as closed.
/// </param>
internal readonly record struct RenderFrameOutcome(
WorldRenderFrameOutcome World,
PrivatePresentationFrameOutcome Presentation);
PrivatePresentationFrameOutcome Presentation,
bool SkippedZeroArea = false)
{
internal static RenderFrameOutcome ZeroArea => new(default, default, SkippedZeroArea: true);
}
internal interface IRenderFrameLifetime
{
@ -191,6 +205,13 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
public RenderFrameOutcome Render(RenderFrameInput input)
{
// A window with no area has nothing to render and no swapchain to
// render into; decide that BEFORE a GPU frame is opened so nothing
// downstream (the render-pack activation extent, the world target,
// the presentation viewport) ever sees a zero extent.
if (input.ViewportWidth <= 0 || input.ViewportHeight <= 0)
return RenderFrameOutcome.ZeroArea;
_lifetime.BeginFrame();
Exception? renderFailure = null;
try