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:
parent
b017391b98
commit
38def07edb
4 changed files with 84 additions and 3 deletions
|
|
@ -24,6 +24,36 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## #424 — Client crashed on alt-tab out of exclusive fullscreen: zero-area frame reached `RenderPackActivationExtent.Validate`
|
||||
|
||||
**Status:** ✅ FIXED 2026-08-23 (same session it was found — the owner's VM6/VM3 gate launch).
|
||||
**Component:** rendering / frame orchestration (host minimised-window guard)
|
||||
|
||||
**Symptom:** exclusive fullscreen 2560×1440, High pack, alt-tab to the desktop →
|
||||
`Unhandled exception. System.ArgumentOutOfRangeException: Activation extent must be
|
||||
positive. (Parameter 'Width')` at `RenderPackController.ApplyAtFrameBoundary`
|
||||
← `VulkanWorldScenePhase.Render` ← `GameWindow.OnRender`. Log:
|
||||
`artifacts/owner-gate/launch.err`.
|
||||
|
||||
**Root cause:** GLFW auto-iconifies an exclusive-fullscreen window on focus
|
||||
loss. `GameWindow.OnRender` guards minimised windows through
|
||||
`VulkanGraphicsContext.PrepareFrame()`, but that only consults a *pending*
|
||||
recreate / swapchain existence — for the one frame between iconify and the
|
||||
present that reports OUT_OF_DATE, `_window.Size` already reads 0×0 while
|
||||
the swapchain is still "created", so a zero-area `RenderFrameInput` went
|
||||
down the full pipeline. Pre-campaign the retail path tolerated a 0×0
|
||||
viewport silently; Campaign AR's pack controller (correctly) refuses a
|
||||
zero activation extent, turning the latent zero-area frame into a crash.
|
||||
|
||||
**Fix:** `RenderFrameOrchestrator.Render` returns
|
||||
`RenderFrameOutcome.ZeroArea` (flag `SkippedZeroArea`) BEFORE
|
||||
`BeginFrame` when either viewport dimension is ≤ 0 — no GPU frame, phase,
|
||||
measurement, diagnostics or recovery runs — and `GameWindow.OnRender` skips
|
||||
`NoteFrameClosed` for such a frame. Test:
|
||||
`RenderFrameOrchestratorTests.ZeroAreaViewport_SkipsTheFrameBeforeAnyGpuWork`
|
||||
(four extents). Not a workaround: a window with no area has nothing to
|
||||
render, and the rule now lives at the one seam every frame passes through.
|
||||
|
||||
## #423 — Atmospheric ray/shadow/volumetric day-group policy keys on the raw day-group index, not the DAT-classified WeatherKind
|
||||
|
||||
**Status:** OPEN — filed 2026-08-23 at Campaign VM VM6 (spot-check); pre-existing from Campaign AR.
|
||||
|
|
|
|||
|
|
@ -1748,6 +1748,7 @@ public sealed class GameWindow :
|
|||
_renderLoopArmed = false;
|
||||
return;
|
||||
}
|
||||
AcDream.App.Rendering.RenderFrameOutcome outcome;
|
||||
try
|
||||
{
|
||||
_frameGraphs.Render(
|
||||
|
|
@ -1755,7 +1756,7 @@ public sealed class GameWindow :
|
|||
deltaSeconds,
|
||||
size.X,
|
||||
size.Y),
|
||||
out _);
|
||||
out outcome);
|
||||
}
|
||||
catch (AcDream.App.Rendering.Gpu.Vk.VulkanSwapchainOutOfDateException)
|
||||
when (_vulkanGraphics is not null)
|
||||
|
|
@ -1765,7 +1766,11 @@ public sealed class GameWindow :
|
|||
return;
|
||||
}
|
||||
|
||||
_vulkanGraphics?.NoteFrameClosed();
|
||||
// A zero-area frame (window iconified between the swapchain check
|
||||
// above and the size read — see RenderFrameOutcome.SkippedZeroArea)
|
||||
// opened no GPU frame, so there is no present outcome to note.
|
||||
if (!outcome.SkippedZeroArea)
|
||||
_vulkanGraphics?.NoteFrameClosed();
|
||||
_renderLoopArmed = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -59,6 +59,31 @@ public sealed class RenderFrameOrchestratorTests
|
|||
Assert.Equal(outcome, phases.ObservedOutcome);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 1080)]
|
||||
[InlineData(1920, 0)]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(-1, 1080)]
|
||||
public void ZeroAreaViewport_SkipsTheFrameBeforeAnyGpuWork(int width, int height)
|
||||
{
|
||||
// Campaign VM VM7 owner gate (2026-08-23): alt-tabbing out of
|
||||
// exclusive fullscreen auto-iconifies the window; for one frame the
|
||||
// size reads 0x0 while the swapchain is still current, and the
|
||||
// zero-area frame crashed the client in
|
||||
// RenderPackActivationExtent.Validate. The rule lives here, before
|
||||
// BeginFrame, so no phase, measurement, diagnostics or recovery can
|
||||
// observe a zero extent, and the outcome says so for the host.
|
||||
var calls = new List<string>();
|
||||
var phases = new RecordingPhases(calls);
|
||||
|
||||
RenderFrameOutcome outcome = Create(phases).Render(
|
||||
Input with { ViewportWidth = width, ViewportHeight = height });
|
||||
|
||||
Assert.True(outcome.SkippedZeroArea);
|
||||
Assert.Empty(calls);
|
||||
Assert.Empty(phases.ObservedInputs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginFailure_DoesNotAttemptAnyPhaseOrClose()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue