270 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|