fix #425: pack resident budget is a 1080p ceiling that scales with pixel count; an explicit Apply retries a failed selection

Live Holtburg at 2560x1440: the Low preset needed 67,368,164 resident bytes
(screen-sized HDR/depth/ray targets are 44 MB of that) against an absolute
64 MiB ceiling that had only been validated at 1080p, so Options -> Apply
fell back to the default path; every later Apply was then refused by the
controller's failure memo, which treated the user's deliberate choice like
automatic re-activation.

RenderPackResidentBudget.Effective scales the declared 1080p figure by the
viewport's pixel-count ratio (never below 1), still capped by the hardware
MaxPackResidentBytes; both pack graphs use it and the performance-matrix
tool judges its resident column by the same rule (contract test updated).
RenderPackController.Request gains explicitUserChoice, which clears the
memo for that selection; RenderPackSelectionBinding passes it on every
display edge (Apply, including resolution changes) and keeps the memo for
the startup request.

Tests: RenderPackResidentBudgetTests (1080p/720p keep the declared
ceiling, 1440p = 16/9x, 4K = 4x, hardware cap wins, zero extent rejected);
controller explicit-retry; the binding test now proves the user's next
Apply activates once the cause is gone. App hermetic lane 6,068/0 (Release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 10:00:11 +02:00
parent 38def07edb
commit 132395e6f7
11 changed files with 250 additions and 8 deletions

View file

@ -692,8 +692,10 @@ internal sealed class AtmosphericPostProcessGraph :
}
TargetSet? previous = _targets;
_targets = candidate;
_residentGpuBudgetBytes = Math.Min(
_residentGpuBudgetBytes = RenderPackResidentBudget.Effective(
Preset.MaxResidentGpuBytes,
width,
height,
capabilities.MaxPackResidentBytes);
ResourceGeneration = checked(ResourceGeneration + 1);
_cpuStageProfiler?.Reset();

View file

@ -268,8 +268,10 @@ internal class DeclaredFullscreenRenderPackGraph :
TargetSet? prior = _targets;
_targets = candidate;
_resourceBudget = budget;
_residentGpuBudgetBytes = Math.Min(
_residentGpuBudgetBytes = RenderPackResidentBudget.Effective(
Preset.MaxResidentGpuBytes,
width,
height,
capabilities.MaxPackResidentBytes);
_resourceGeneration = checked(_resourceGeneration + 1);
_renderedFrame = false;

View file

@ -272,10 +272,25 @@ internal sealed class RenderPackController :
};
}
internal void Request(RenderPackSelectionSettings? selection)
/// <param name="explicitUserChoice">
/// True when the request is a deliberate user action (the Options panel's
/// Apply, which also fires for a resolution change). Such a request gets a
/// fresh activation attempt even if the same selection failed earlier in
/// this registration: the conditions that failed it — resolution, scene,
/// resident budget — may have changed, and the user asked. Automatic
/// re-activation (startup, catalog change) keeps the failure memo, which
/// exists to stop a known-bad selection from failing every frame.
/// (#425, Campaign VM VM7 owner gate: a Low preset that failed its budget
/// once locked the user out of the pack until restart.)
/// </param>
internal void Request(
RenderPackSelectionSettings? selection,
bool explicitUserChoice = false)
{
ObjectDisposedException.ThrowIf(_disposed, this);
RenderPackSelectionSettings normalized = Normalize(selection);
if (explicitUserChoice)
_failedSelections.Remove(normalized);
if (normalized == _snapshot.Selection
&& _pending is null)
return;

View file

@ -0,0 +1,55 @@
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// The one rule for a pack preset's effective resident-GPU ceiling.
/// </summary>
/// <remarks>
/// <para>
/// A preset's <c>MaxResidentGpuBytes</c> is the <b>1080p</b> figure of the
/// Campaign AR budget table (Low 64 / Medium 128 / High 256 MiB). The pack's
/// resident set is dominated by screen-sized images — the HDR world colour +
/// depth targets alone are 12 bytes per pixel, plus the ray/bloom
/// intermediates — so a ceiling that does not scale with pixel count refuses
/// the pack the moment the user picks a larger mode: at 2560×1440 the Low
/// preset's screen targets are 44 MB of a 64 MiB budget, and live Holtburg's
/// scene-dependent shadow command buffers pushed the total to 64.25 MiB.
/// That refusal is what the owner hit at the Campaign VM VM7 gate
/// (2026-08-23, #425): Apply → "needs 67368164 resident GPU bytes; the
/// active pack budget is 67108864" → persisted back to the default path.
/// </para>
/// <para>
/// The effective ceiling is therefore the declared 1080p figure scaled by
/// the pixel-count ratio (never below 1, so smaller modes keep the declared
/// ceiling), and still capped by the hardware's <c>MaxPackResidentBytes</c>.
/// The parts of the resident set that do NOT scale with the screen (shadow
/// depth maps, shadow command/transform buffers) are over-allowed by the same
/// factor; that errs on the side of honouring the user's explicit resolution
/// choice while keeping the bound proportional to what they asked the GPU to
/// draw. <c>tools/run-atmospheric-performance-matrix.ps1</c> judges its
/// resident column with this same rule so the tool and the runtime cannot
/// disagree.
/// </para>
/// </remarks>
internal static class RenderPackResidentBudget
{
internal const long ReferencePixels = 1920L * 1080L;
internal static long Effective(
long declaredBytesAt1080p,
int viewportWidth,
int viewportHeight,
long hardwareCapBytes)
{
ArgumentOutOfRangeException.ThrowIfNegative(declaredBytesAt1080p);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(viewportWidth);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(viewportHeight);
ArgumentOutOfRangeException.ThrowIfNegative(hardwareCapBytes);
long pixels = checked((long)viewportWidth * viewportHeight);
long scaled = pixels <= ReferencePixels
? declaredBytesAt1080p
: checked((long)Math.Ceiling(
(double)declaredBytesAt1080p * pixels / ReferencePixels));
return Math.Min(scaled, hardwareCapBytes);
}
}

View file

@ -79,7 +79,11 @@ internal sealed class RenderPackSelectionBinding : IDisposable
private void OnDisplayChanged(DisplaySettings display)
{
// A display edge is the user's Apply (pack, preset or resolution) —
// an explicit choice, so a selection that failed earlier gets one
// fresh attempt (#425). The constructor's startup request above is
// automatic and keeps the memo.
if (!_disposed && !_suppressDisplayEdge)
_controller.Request(display.RenderPack);
_controller.Request(display.RenderPack, explicitUserChoice: true);
}
}