feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -0,0 +1,270 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue