acdream/src/AcDream.App/Rendering/Packs/RenderPackResourceBudgetPlanner.cs

270 lines
11 KiB
C#

using AcDream.Plugin.Abstractions.Rendering;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct RenderPackResourceBudget(
long RetainedGpuBytes,
long MultisampleGpuBytes,
int LargestImageWidth,
int LargestImageHeight,
int LargestImageLayerCount)
{
internal long TotalGpuBytes => checked(RetainedGpuBytes + MultisampleGpuBytes);
}
/// <summary>
/// Resolves declaration extents against the real main-world size before an
/// executor allocates any size-dependent image. Declared byte estimates are
/// useful during discovery, but cannot prove a 1080p/1440p/4K preset ceiling.
/// This is the allocation-time authority for the images API-v1 executors
/// actually keep alive.
/// </summary>
internal static class RenderPackResourceBudgetPlanner
{
private const int HdrColorBytesPerPixel = 8;
private const int LdrColorBytesPerPixel = 4;
private const int DirectionalDepthBytesPerPixel = 4;
private const int MainWorldDepthBytesPerPixel = 4;
// Production Vulkan owns two frame-flight slots. Directional shadows
// materialize one shared demand-growth N.5 transform arena in each slot before an
// ordinary world frame can consume the pack, so admission must include
// those mandatory buffers rather than discovering them after the first
// shadow pass has already published a borrow.
private const int DirectionalShadowTransformFlightSlots = 2;
internal static RenderPackResourceBudget Resolve(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount)
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(preset);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(mainWorldWidth);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(mainWorldHeight);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sampleCount);
// Every executable graph replaces the main world attachment with one
// RGBA16F colour image and one D24S8 depth image. The resolve images
// remain alive for the complete active target set.
long mainPixels = checked((long)mainWorldWidth * mainWorldHeight);
long retained = checked(mainPixels
* (HdrColorBytesPerPixel + MainWorldDepthBytesPerPixel));
long multisample = sampleCount > 1
? checked(mainPixels
* (HdrColorBytesPerPixel + MainWorldDepthBytesPerPixel)
* sampleCount)
: 0L;
int largestWidth = mainWorldWidth;
int largestHeight = mainWorldHeight;
int largestLayers = 1;
HashSet<string> writtenResources = descriptor.Passes
.SelectMany(static pass => pass.ResourceWrites)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (RenderResourceDeclaration resource in descriptor.Resources)
{
if (resource.Semantic == RenderResourceSemantic.MainWorldHdr
|| !writtenResources.Contains(resource.Id))
{
continue;
}
if (UsesFusedAtmosphericPostProcess(preset)
&& resource.Semantic is RenderResourceSemantic.BloomPing
or RenderResourceSemantic.BloomPong)
{
// The fused Low filmic shader evaluates the declared bloom
// extraction/filter directly from world colour + sun rays.
// These ping/pong images have no executing writer or reader.
continue;
}
if (resource.Kind is not RenderResourceKind.Image2D
and not RenderResourceKind.Image2DArray)
{
throw new NotSupportedException(
$"Resource '{resource.Id}' is not an API-v1 image resource.");
}
RenderQualityResourceOverride? resourceOverride = preset.ResourceOverrides
.FirstOrDefault(value => string.Equals(
value.ResourceId,
resource.Id,
StringComparison.OrdinalIgnoreCase));
RenderExtentDeclaration extent = resourceOverride?.Extent
?? resource.Extent
?? throw new NotSupportedException(
$"Image resource '{resource.Id}' has no extent.");
(int width, int height) = ResolveExtent(
resource.Id,
extent,
mainWorldWidth,
mainWorldHeight);
int layers = extent.Layers;
if (layers <= 0)
{
throw new NotSupportedException(
$"Image resource '{resource.Id}' has no image layers.");
}
int bytesPerPixel = resource.Format switch
{
RenderFormatClass.HdrColor => HdrColorBytesPerPixel,
RenderFormatClass.LdrColor or RenderFormatClass.SingleChannel =>
LdrColorBytesPerPixel,
RenderFormatClass.DirectionalDepth => DirectionalDepthBytesPerPixel,
_ => throw new NotSupportedException(
$"Image resource '{resource.Id}' has unsupported format "
+ $"'{resource.Format}'."),
};
retained = checked(retained
+ ((long)width * height * layers * bytesPerPixel));
largestWidth = Math.Max(largestWidth, width);
largestHeight = Math.Max(largestHeight, height);
largestLayers = Math.Max(largestLayers, layers);
}
if (descriptor.Passes.Any(pass =>
pass.Semantic == RenderPassSemantic.DirectionalShadowDepth))
{
retained = checked(
retained
+ DirectionalShadowTransformFlightSlots
* WorldTransformCapacityPolicy.InitialBindingSizeBytes);
}
return new RenderPackResourceBudget(
retained,
multisample,
largestWidth,
largestHeight,
largestLayers);
}
private static bool UsesFusedAtmosphericPostProcess(
RenderQualityPreset preset) =>
(preset.ExecutionHints
& RenderQualityExecutionHints.FusedAtmosphericPostProcess) != 0;
internal static RenderPackResourceBudget RequireWithinPreset(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount)
{
RenderPackResourceBudget budget = Resolve(
descriptor,
preset,
mainWorldWidth,
mainWorldHeight,
sampleCount);
if (budget.RetainedGpuBytes > preset.MaxResidentGpuBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.RetainedGpuBytes} resident GPU bytes at "
+ $"{mainWorldWidth}x{mainWorldHeight}; its declared ceiling is "
+ $"{preset.MaxResidentGpuBytes}. Select a compatible preset or "
+ "reduce the main-world resolution.");
}
return budget;
}
/// <summary>
/// Allocation-time gate against the selected adapter and the host's
/// explicit optional-memory share. Catalog checks can reject absolute
/// preset extents, but only this point knows the resolved viewport-relative
/// sizes and multisample attachment bytes.
/// </summary>
internal static RenderPackResourceBudget RequireWithinHost(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount,
RenderPackHostCapabilities capabilities)
{
ArgumentNullException.ThrowIfNull(capabilities);
RenderPackResourceBudget budget = RequireWithinPreset(
descriptor,
preset,
mainWorldWidth,
mainWorldHeight,
sampleCount);
if (budget.LargestImageWidth > capabilities.MaxImageDimension2D
|| budget.LargestImageHeight > capabilities.MaxImageDimension2D)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' resolves an image to "
+ $"{budget.LargestImageWidth}x{budget.LargestImageHeight} at "
+ $"{mainWorldWidth}x{mainWorldHeight}; this device's maximum "
+ $"2-D image edge is {capabilities.MaxImageDimension2D}.");
}
if (budget.LargestImageLayerCount > capabilities.MaxImageArrayLayers)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.LargestImageLayerCount} image-array layers; this "
+ $"device provides {capabilities.MaxImageArrayLayers}.");
}
if (budget.RetainedGpuBytes > capabilities.MaxPackResidentBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.RetainedGpuBytes} resident GPU bytes at "
+ $"{mainWorldWidth}x{mainWorldHeight}; this host permits "
+ $"{capabilities.MaxPackResidentBytes} under its "
+ $"{capabilities.MemoryPolicyDescription} policy.");
}
if (budget.MultisampleGpuBytes > capabilities.MaxPackTransientBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.MultisampleGpuBytes} transient multisample GPU bytes "
+ $"at {mainWorldWidth}x{mainWorldHeight} x{sampleCount}; this "
+ $"host permits {capabilities.MaxPackTransientBytes} under its "
+ $"{capabilities.MemoryPolicyDescription} policy.");
}
return budget;
}
private static (int Width, int Height) ResolveExtent(
string resourceId,
RenderExtentDeclaration extent,
int mainWorldWidth,
int mainWorldHeight)
{
if (!double.IsFinite(extent.Width)
|| !double.IsFinite(extent.Height)
|| extent.Width <= 0d
|| extent.Height <= 0d)
{
throw new NotSupportedException(
$"Image resource '{resourceId}' has an invalid extent.");
}
try
{
return extent.Mode switch
{
RenderExtentMode.AbsolutePixels =>
(checked((int)extent.Width), checked((int)extent.Height)),
RenderExtentMode.RelativeToMainWorld or RenderExtentMode.RelativeToOutput =>
(Math.Max(1, checked((int)Math.Ceiling(mainWorldWidth * extent.Width))),
Math.Max(1, checked((int)Math.Ceiling(mainWorldHeight * extent.Height)))),
_ => throw new NotSupportedException(
$"Image resource '{resourceId}' has unsupported extent mode "
+ $"'{extent.Mode}'."),
};
}
catch (OverflowException error)
{
throw new NotSupportedException(
$"Image resource '{resourceId}' extent overflows the host image range.",
error);
}
}
}