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
|
|
@ -51,7 +51,9 @@ internal sealed record FrameRootDependencies(
|
|||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
|
||||
UpdateFrameClock UpdateClock,
|
||||
GameFrameGraphSlot FrameGraphs,
|
||||
Action<string> Log)
|
||||
Action<string> Log,
|
||||
AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public RuntimeLocalPlayerMovementState PlayerController =>
|
||||
Runtime.MovementOwner;
|
||||
|
|
@ -234,6 +236,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
ref bool bindingsOwnedByScope)
|
||||
{
|
||||
FrameRootDependencies d = _dependencies;
|
||||
bindings = new FrameRootRuntimeBindings();
|
||||
WorldRenderFoundation foundation = world.Foundation;
|
||||
// Campaign V slice V6h: the frame root's raw-GL render graph fork was
|
||||
// deleted at slice V11. The graph is the clear pass, the private-
|
||||
|
|
@ -285,6 +288,35 @@ internal sealed class FrameRootCompositionPhase
|
|||
renderFrameLivePreparation);
|
||||
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
|
||||
|
||||
AcDream.App.Rendering.Packs.RenderPackController? renderPackController = null;
|
||||
AcDream.App.Rendering.Packs.RenderPackSelectionBinding? renderPackSelection = null;
|
||||
AcDream.App.Rendering.Packs.AtmosphericFrameInputState? atmosphericInputs = null;
|
||||
if (settings.RenderPacks is { } renderPackCatalog)
|
||||
{
|
||||
renderPackController = new AcDream.App.Rendering.Packs.RenderPackController(
|
||||
renderPackCatalog.Snapshot,
|
||||
new AcDream.App.Rendering.Packs.AtmosphericRenderPackRuntimeFactory(
|
||||
host.GpuDevice),
|
||||
new AcDream.App.Rendering.Packs.RenderPackReceiverPipelineCoordinator(
|
||||
foundation.Terrain!,
|
||||
live.DrawDispatcher!),
|
||||
AcDream.App.Rendering.Packs.ThreadPoolRenderPackPreparationScheduler.Instance,
|
||||
renderPackCatalog);
|
||||
bindings.Adopt("render-pack controller", renderPackController);
|
||||
renderPackSelection = new AcDream.App.Rendering.Packs.RenderPackSelectionBinding(
|
||||
d.Settings,
|
||||
renderPackController,
|
||||
d.Log);
|
||||
bindings.Adopt("render-pack selection", renderPackSelection);
|
||||
if (d.RenderPackDiagnostics is { } renderPackDiagnostics)
|
||||
{
|
||||
bindings.Adopt(
|
||||
"render-pack diagnostics",
|
||||
renderPackDiagnostics.BindOwned(renderPackController));
|
||||
}
|
||||
atmosphericInputs = new AcDream.App.Rendering.Packs.AtmosphericFrameInputState();
|
||||
}
|
||||
|
||||
var renderWeatherFrame = new RenderWeatherFrameController(
|
||||
d.WorldTime,
|
||||
d.Weather);
|
||||
|
|
@ -463,7 +495,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
worldScenePasses,
|
||||
d.RenderRange,
|
||||
worldSceneDiagnostics,
|
||||
live.WorldAvailability);
|
||||
live.WorldAvailability,
|
||||
atmosphericInputs);
|
||||
// The world renderer runs INSIDE the frame's one backbuffer pass,
|
||||
// which this phase opens, publishes on the scope, and closes.
|
||||
worldSceneRenderer =
|
||||
|
|
@ -474,14 +507,70 @@ internal sealed class FrameRootCompositionPhase
|
|||
(d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
|
||||
?? throw new InvalidOperationException(
|
||||
"The Vulkan world phase requires the Vulkan graphics handle."),
|
||||
worldSceneRenderer);
|
||||
worldSceneRenderer,
|
||||
renderPackController,
|
||||
atmosphericInputs,
|
||||
renderPackSelection is null
|
||||
? null
|
||||
: renderPackSelection.ApplyAtFrameBoundary,
|
||||
live.RenderSceneShadow,
|
||||
live.DrawDispatcher,
|
||||
foundation.Terrain);
|
||||
}
|
||||
Fault(FrameRootCompositionPoint.WorldRendererCreated);
|
||||
bindings = new FrameRootRuntimeBindings();
|
||||
WorldLifecycleAutomationController? lifecycleAutomation = null;
|
||||
if (interaction.RetainedUi?.Screenshots is { } screenshots
|
||||
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings?
|
||||
automationLastEnhancedSelection = null;
|
||||
|
||||
(bool Succeeded, string Error) SaveAutomationRenderPackSelection(
|
||||
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings selection)
|
||||
{
|
||||
d.Settings.SaveDisplay(d.Settings.Display with
|
||||
{
|
||||
RenderPack = selection,
|
||||
});
|
||||
return d.Settings.Display.RenderPack == selection
|
||||
? (true, string.Empty)
|
||||
: (false, $"render-pack selection '{selection.PresetId}' was not persisted");
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) SelectAutomationRenderPack(string preset)
|
||||
{
|
||||
var selection = string.Equals(
|
||||
preset,
|
||||
"retail",
|
||||
StringComparison.Ordinal)
|
||||
? AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail
|
||||
: new AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings(
|
||||
AcDream.App.Rendering.Packs
|
||||
.BuiltInAtmosphericRenderPack.Id,
|
||||
"1.0.0",
|
||||
preset);
|
||||
return SaveAutomationRenderPackSelection(selection);
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) DisableAutomationRenderPack()
|
||||
{
|
||||
var current = d.Settings.Display.RenderPack;
|
||||
if (!current.IsRetail)
|
||||
automationLastEnhancedSelection = current;
|
||||
return SaveAutomationRenderPackSelection(
|
||||
AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail);
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) ReenableAutomationRenderPack()
|
||||
{
|
||||
return automationLastEnhancedSelection is { } selection
|
||||
? SaveAutomationRenderPackSelection(selection)
|
||||
: (false, "render-pack re-enable requires a prior enhanced selection");
|
||||
}
|
||||
|
||||
var resourceSnapshots =
|
||||
new WorldLifecycleResourceSnapshotSource(
|
||||
live.WorldState,
|
||||
|
|
@ -515,7 +604,86 @@ internal sealed class FrameRootCompositionPhase
|
|||
resourceSnapshots.Capture,
|
||||
screenshots,
|
||||
artifactDirectory,
|
||||
message => d.Log("[UI-PROBE] " + message));
|
||||
message => d.Log("[UI-PROBE] " + message),
|
||||
() => renderPackController?.MinimumPerformanceSampleCount ?? 0,
|
||||
() =>
|
||||
{
|
||||
if (renderPackController is null)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
"render-pack performance automation is unavailable");
|
||||
}
|
||||
bool reset = renderPackController.TryResetPerformanceEvidence(
|
||||
out string error);
|
||||
return (reset, error);
|
||||
},
|
||||
() => renderPackController?.Snapshot.State ==
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail,
|
||||
getRenderPackStatus: () =>
|
||||
{
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationSnapshot snapshot =
|
||||
renderPackController?.Snapshot
|
||||
?? new AcDream.App.Rendering.Packs.RenderPackActivationSnapshot(
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail,
|
||||
AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail,
|
||||
ActivePackDisplayName: null,
|
||||
Reason: null,
|
||||
ActivationGeneration: 0);
|
||||
var state = snapshot.State switch
|
||||
{
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.Retail,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.CandidatePending =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.CandidatePending,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Active =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.Active,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.FailedToRetail,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
return new AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackStatus(
|
||||
state,
|
||||
snapshot.Selection.PackId,
|
||||
snapshot.Selection.PresetId,
|
||||
snapshot.ActivationGeneration,
|
||||
snapshot.Reason);
|
||||
},
|
||||
selectRenderPack: SelectAutomationRenderPack,
|
||||
disableRenderPack: DisableAutomationRenderPack,
|
||||
reenableRenderPack: ReenableAutomationRenderPack,
|
||||
getFramebufferSize: () =>
|
||||
{
|
||||
var size = d.Window.FramebufferSize;
|
||||
return (size.X, size.Y);
|
||||
},
|
||||
resizeFramebuffer: (width, height) =>
|
||||
{
|
||||
if (d.Settings.Display.Fullscreen)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
"automation framebuffer resize requires windowed mode");
|
||||
}
|
||||
string resolution = $"{width}x{height}";
|
||||
d.Settings.SaveDisplay(d.Settings.Display with
|
||||
{
|
||||
Resolution = resolution,
|
||||
});
|
||||
return string.Equals(
|
||||
d.Settings.Display.Resolution,
|
||||
resolution,
|
||||
StringComparison.Ordinal)
|
||||
? (true, string.Empty)
|
||||
: (false, $"framebuffer resize '{resolution}' was not persisted");
|
||||
},
|
||||
requestClientClose: d.Window.Close);
|
||||
bindings.Adopt(
|
||||
"world lifecycle automation owner",
|
||||
lifecycleAutomation);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ internal sealed record HostInputCameraDependencies(
|
|||
LocalPlayerModeState LocalPlayerMode,
|
||||
ChaseCameraInputState ChaseCameraInput,
|
||||
PointerPositionState PointerPosition,
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog);
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog,
|
||||
float? InitialOrbitDistanceMeters = null,
|
||||
float? InitialOrbitYawDegrees = null,
|
||||
float? InitialOrbitPitchDegrees = null);
|
||||
|
||||
/// <summary>
|
||||
/// The construction seam every backend differs at. Campaign V slice V6h widened
|
||||
|
|
@ -101,7 +104,10 @@ internal interface IHostInputCameraCompositionFactory
|
|||
IKeyboardSource keyboard,
|
||||
IMouseSource mouse,
|
||||
KeyBindings bindings);
|
||||
CameraController CreateCameraController();
|
||||
CameraController CreateCameraController(
|
||||
float? initialOrbitDistanceMeters,
|
||||
float? initialOrbitYawDegrees,
|
||||
float? initialOrbitPitchDegrees);
|
||||
IFramebufferCameraTarget CreateCameraTarget(CameraController camera);
|
||||
CameraPointerInputController CreateCameraPointerInput(
|
||||
IReadOnlyList<IMouse> mice,
|
||||
|
|
@ -311,7 +317,10 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
Fault(HostInputCameraCompositionPoint.CameraInputBound);
|
||||
}
|
||||
|
||||
CameraController camera = _factory.CreateCameraController();
|
||||
CameraController camera = _factory.CreateCameraController(
|
||||
_dependencies.InitialOrbitDistanceMeters,
|
||||
_dependencies.InitialOrbitYawDegrees,
|
||||
_dependencies.InitialOrbitPitchDegrees);
|
||||
_publication.PublishCameraController(camera);
|
||||
Fault(HostInputCameraCompositionPoint.CameraPublished);
|
||||
_dependencies.FramebufferResize.BindCamera(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,10 @@ internal sealed record InteractionRetainedUiDependencies(
|
|||
// needed. gmMapUI::Update @0x004a1eb0 reads GameTime::current_game_time
|
||||
// every 5s — MapPageController owns that cadence, this just supplies the
|
||||
// current reading.
|
||||
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar)
|
||||
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar,
|
||||
AcDream.App.Rendering.Packs.RenderPackCatalogSource? RenderPackCatalog = null,
|
||||
Func<AcDream.App.Rendering.Packs.RenderPackDiagnosticsSnapshot>?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public RuntimeActionState Actions => Runtime.ActionOwner;
|
||||
|
||||
|
|
@ -643,7 +646,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
screenshots = new FrameScreenshotController(
|
||||
d.BackbufferReader,
|
||||
Path.Combine(artifactDirectory, "screenshots"),
|
||||
ProbeLog);
|
||||
ProbeLog,
|
||||
d.RenderPackDiagnostics);
|
||||
}
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiProbeCreated);
|
||||
|
||||
|
|
@ -949,7 +953,53 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
LoadDisplay: () => d.Settings.Display,
|
||||
SaveDisplay: d.Settings.SaveDisplay,
|
||||
LoadAudio: () => d.Settings.Audio,
|
||||
SaveAudio: d.Settings.SaveAudio),
|
||||
SaveAudio: d.Settings.SaveAudio,
|
||||
LoadRenderPackChoices: d.RenderPackCatalog is null
|
||||
? null
|
||||
: () => d.RenderPackCatalog.Snapshot().Entries
|
||||
.Select(entry =>
|
||||
new ConfigOptionsPageController.RenderPackChoice(
|
||||
entry.Descriptor.Id,
|
||||
entry.Descriptor.DisplayName,
|
||||
entry.Descriptor.PackVersion.ToString(),
|
||||
entry.IsCompatible,
|
||||
entry.IncompatibilityReason,
|
||||
entry.Descriptor.QualityPresets
|
||||
.Select(preset =>
|
||||
{
|
||||
entry.PresetIncompatibilityReasons.TryGetValue(
|
||||
preset.Id,
|
||||
out string? reason);
|
||||
return new ConfigOptionsPageController.RenderPackPresetChoice(
|
||||
preset.Id,
|
||||
preset.DisplayName,
|
||||
entry.IsCompatible && reason is null,
|
||||
reason ?? entry.IncompatibilityReason)
|
||||
{
|
||||
SettingOverrides = preset.SettingOverrides,
|
||||
MaxResidentGpuBytes = preset.MaxResidentGpuBytes,
|
||||
MaxIncrementalGpuMillisecondsP50 =
|
||||
preset.MaxIncrementalGpuMillisecondsP50,
|
||||
MaxIncrementalGpuMillisecondsP99 =
|
||||
preset.MaxIncrementalGpuMillisecondsP99,
|
||||
MaxIncrementalCpuMillisecondsP50 =
|
||||
preset.MaxIncrementalCpuMillisecondsP50,
|
||||
MaxIncrementalCpuMillisecondsP99 =
|
||||
preset.MaxIncrementalCpuMillisecondsP99,
|
||||
};
|
||||
})
|
||||
.ToArray())
|
||||
{
|
||||
FeatureSummary = entry.Descriptor.FeatureSummary,
|
||||
Settings = entry.Descriptor.Settings,
|
||||
})
|
||||
.ToArray(),
|
||||
LoadRenderPackCatalogRevision: d.RenderPackCatalog is null
|
||||
? null
|
||||
: () => d.RenderPackCatalog.Revision,
|
||||
LoadRenderPackFailureNotice: d.RenderPackDiagnostics is null
|
||||
? null
|
||||
: () => d.RenderPackDiagnostics().FailureReason),
|
||||
// Campaign FA slice FA3: the social panel's own bindings —
|
||||
// FA2's typed Fellowship/Allegiance snapshot readers off the
|
||||
// GameRuntime views, plus J4.1's Friends/Squelch owners
|
||||
|
|
|
|||
|
|
@ -792,6 +792,67 @@ internal sealed class DeferredWorldLifecycleAutomationRuntime
|
|||
!_deactivated && _target?.IsWorldViewportVisible == true;
|
||||
public int PortalMaterializationCount =>
|
||||
!_deactivated ? _target?.PortalMaterializationCount ?? 0 : 0;
|
||||
public int RenderPackPerformanceSampleCount =>
|
||||
!_deactivated ? _target?.RenderPackPerformanceSampleCount ?? 0 : 0;
|
||||
public bool RenderPackFailedToRetail =>
|
||||
!_deactivated && _target?.RenderPackFailedToRetail == true;
|
||||
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
|
||||
!_deactivated
|
||||
? _target?.RenderPackStatus
|
||||
?? RetailUiAutomationRenderPackStatus.Retail
|
||||
: RetailUiAutomationRenderPackStatus.Retail;
|
||||
public int FramebufferWidth =>
|
||||
!_deactivated ? _target?.FramebufferWidth ?? 0 : 0;
|
||||
public int FramebufferHeight =>
|
||||
!_deactivated ? _target?.FramebufferHeight ?? 0 : 0;
|
||||
|
||||
public bool TrySelectRenderPack(string presetId, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TrySelectRenderPack(presetId, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDisableRenderPack(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryDisableRenderPack(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryReenableRenderPack(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryReenableRenderPack(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResizeFramebuffer(int width, int height, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryResizeFramebuffer(width, height, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResetRenderPackPerformance(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryResetRenderPackPerformance(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRequestClientClose(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryRequestClientClose(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public IDisposable Bind(IRetailUiAutomationRuntime target)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,7 +74,9 @@ internal sealed record LivePresentationDependencies(
|
|||
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
|
||||
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
|
||||
Action<string> Log,
|
||||
Action<string>? Toast)
|
||||
Action<string>? Toast,
|
||||
AcDream.App.Rendering.Packs.IRenderPackDiagnosticsSnapshotSource?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public SelectionState Selection => Runtime.ActionOwner.Selection;
|
||||
|
||||
|
|
@ -448,7 +450,8 @@ internal sealed class LivePresentationCompositionPhase
|
|||
LiveRenderProjectionJournal? liveRenderProjections =
|
||||
renderSceneShadow?.BindLiveRuntime(
|
||||
liveEntities,
|
||||
new GpuWorldRenderTraversalOrderSource(worldState));
|
||||
new GpuWorldRenderTraversalOrderSource(worldState),
|
||||
d.PlayerIdentity);
|
||||
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
||||
|
||||
bindings.Adopt(
|
||||
|
|
@ -803,7 +806,9 @@ internal sealed class LivePresentationCompositionPhase
|
|||
d.TranslucencyFades,
|
||||
selectionScene,
|
||||
d.RetailAlphaQueue,
|
||||
alphaScratchBudgets.DispatcherBytes),
|
||||
alphaScratchBudgets.DispatcherBytes,
|
||||
foundation.TerrainAtlas?.BuildingDetailTexture ?? default,
|
||||
() => d.Settings.DisplayPreview.BuildingDetailTextures),
|
||||
static value => value.Dispose());
|
||||
var selectionQuery = new WorldSelectionQuery(
|
||||
liveEntities,
|
||||
|
|
@ -1238,9 +1243,11 @@ internal sealed class LivePresentationCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The graphics backend must publish a world pass scope."),
|
||||
foundation.MeshAdapter!.MeshManager!,
|
||||
envCellFrustum),
|
||||
envCellFrustum,
|
||||
foundation.TerrainAtlas?.EnvironmentDetailTexture ?? default,
|
||||
() => d.Settings.DisplayPreview.BuildingDetailTextures),
|
||||
static value => value.Dispose());
|
||||
// The three pipelines ARE its program, built at construction — the
|
||||
// The four pipelines ARE its program, built at construction — the
|
||||
// raw-GL arm's separate Initialize(Shader) step was deleted at V11.
|
||||
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
|
||||
|
||||
|
|
@ -1484,7 +1491,8 @@ internal sealed class LivePresentationCompositionPhase
|
|||
new SilkRenderFrameTitleSink(d.Window),
|
||||
d.RenderDiagnosticLog,
|
||||
d.Options.UiProbeDump,
|
||||
resourceDiagnostics);
|
||||
resourceDiagnostics,
|
||||
d.RenderPackDiagnostics);
|
||||
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
|
||||
{
|
||||
bindings.Adopt(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
using AcDream.App.Settings;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
|
@ -18,11 +22,22 @@ namespace AcDream.App.Composition;
|
|||
/// keybinds.json (not retail's <c>.keymap</c> format — register row AP-202).
|
||||
/// </summary>
|
||||
internal sealed record SettingsDevToolsResult(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality);
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality)
|
||||
{
|
||||
internal RenderPackCatalogSource? RenderPacks { get; init; }
|
||||
|
||||
internal RenderPackSelectionSettings RenderPackSelection { get; init; } =
|
||||
RenderPackSelectionSettings.Retail;
|
||||
}
|
||||
|
||||
internal sealed record SettingsDevToolsDependencies(
|
||||
RuntimeSettingsController Settings,
|
||||
IRuntimeSettingsStartupTarget StartupTarget);
|
||||
IRuntimeSettingsStartupTarget StartupTarget)
|
||||
{
|
||||
internal BufferedRenderPackRegistry? RenderPacks { get; init; }
|
||||
|
||||
internal IGpuDevice? GpuDevice { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 3: applies the resolved startup display/audio settings.
|
||||
|
|
@ -51,6 +66,19 @@ internal sealed class SettingsDevToolsCompositionPhase :
|
|||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);
|
||||
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality);
|
||||
RenderPackCatalogSource? renderPacks = null;
|
||||
if (_dependencies.RenderPacks is { } registry
|
||||
&& _dependencies.GpuDevice is { } gpu)
|
||||
{
|
||||
renderPacks = new RenderPackCatalogSource(
|
||||
registry,
|
||||
RenderPackCapabilityResolver.Resolve(gpu.Capabilities));
|
||||
}
|
||||
|
||||
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality)
|
||||
{
|
||||
RenderPacks = renderPacks,
|
||||
RenderPackSelection = _dependencies.Settings.Display.RenderPack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,23 @@ internal sealed class VulkanHostInputCameraCompositionFactory
|
|||
KeyBindings bindings) =>
|
||||
InputDispatcher.CreateDetached(keyboard, mouse, bindings);
|
||||
|
||||
public CameraController CreateCameraController() =>
|
||||
new(new OrbitCamera(), new FlyCamera());
|
||||
public CameraController CreateCameraController(
|
||||
float? initialOrbitDistanceMeters,
|
||||
float? initialOrbitYawDegrees,
|
||||
float? initialOrbitPitchDegrees)
|
||||
{
|
||||
var orbit = new OrbitCamera();
|
||||
if (initialOrbitDistanceMeters is { } distance)
|
||||
orbit.Distance = distance;
|
||||
if (initialOrbitYawDegrees is { } yaw)
|
||||
orbit.Yaw = DegreesToRadians(yaw);
|
||||
if (initialOrbitPitchDegrees is { } pitch)
|
||||
orbit.Pitch = DegreesToRadians(pitch);
|
||||
return new CameraController(orbit, new FlyCamera());
|
||||
}
|
||||
|
||||
private static float DegreesToRadians(float degrees) =>
|
||||
degrees * (MathF.PI / 180f);
|
||||
|
||||
public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) =>
|
||||
new CameraFramebufferTarget(camera);
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ internal interface IGameWindowWorldRenderPublication
|
|||
internal interface IWorldRenderCompositionFactory
|
||||
{
|
||||
WorldRegionData LoadRegion(IDatReaderWriter dats);
|
||||
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
|
||||
void InitializeEnvironment(
|
||||
WorldEnvironmentController environment,
|
||||
Region region,
|
||||
IDatReaderWriter dats);
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the terrain atlas built through
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
|
||||
|
|
@ -188,11 +191,13 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
|
||||
public void InitializeEnvironment(
|
||||
WorldEnvironmentController environment,
|
||||
Region region)
|
||||
Region region,
|
||||
IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
ArgumentNullException.ThrowIfNull(region);
|
||||
environment.Initialize(region);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
environment.Initialize(region, dats);
|
||||
}
|
||||
|
||||
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
|
||||
|
|
@ -452,7 +457,10 @@ internal sealed class WorldRenderCompositionPhase
|
|||
|
||||
WorldRegionData region = _factory.LoadRegion(content.Dats);
|
||||
Fault(WorldRenderCompositionPoint.RegionLoaded);
|
||||
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
|
||||
_factory.InitializeEnvironment(
|
||||
_dependencies.Environment,
|
||||
region.Region,
|
||||
content.Dats);
|
||||
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
|
||||
|
||||
// Campaign V slice V6i-2: the atlas builds through IGpuDevice on
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
using System.Text.Json;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -22,6 +25,7 @@ internal sealed class FrameScreenshotController
|
|||
private readonly Func<int, int, byte[]> _readRgba;
|
||||
private readonly string _directory;
|
||||
private readonly Action<string> _log;
|
||||
private readonly Func<RenderPackDiagnosticsSnapshot>? _renderPackMetadata;
|
||||
private readonly Queue<string> _pending = new();
|
||||
private readonly Dictionary<string, CaptureStatus> _status =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -29,13 +33,15 @@ internal sealed class FrameScreenshotController
|
|||
internal FrameScreenshotController(
|
||||
Func<int, int, byte[]> readRgba,
|
||||
string directory,
|
||||
Action<string>? log = null)
|
||||
Action<string>? log = null,
|
||||
Func<RenderPackDiagnosticsSnapshot>? renderPackMetadata = null)
|
||||
{
|
||||
_readRgba = readRgba ?? throw new ArgumentNullException(nameof(readRgba));
|
||||
_directory = string.IsNullOrWhiteSpace(directory)
|
||||
? throw new ArgumentException("A screenshot directory is required.", nameof(directory))
|
||||
: Path.GetFullPath(directory);
|
||||
_log = log ?? (_ => { });
|
||||
_renderPackMetadata = renderPackMetadata;
|
||||
}
|
||||
|
||||
public bool TryRequest(string name, out string error)
|
||||
|
|
@ -85,6 +91,27 @@ internal sealed class FrameScreenshotController
|
|||
string temporaryPath = path + ".tmp";
|
||||
using (Image<Rgba32> image = Image.LoadPixelData<Rgba32>(flipped, width, height))
|
||||
image.SaveAsPng(temporaryPath);
|
||||
|
||||
string? metadataPath = null;
|
||||
string? temporaryMetadataPath = null;
|
||||
if (_renderPackMetadata is not null)
|
||||
{
|
||||
metadataPath = Path.Combine(_directory, name + ".metadata.json");
|
||||
temporaryMetadataPath = metadataPath + ".tmp";
|
||||
var metadata = new FrameScreenshotMetadata(
|
||||
SchemaVersion: 1,
|
||||
Width: width,
|
||||
Height: height,
|
||||
RenderPack: _renderPackMetadata());
|
||||
File.WriteAllBytes(
|
||||
temporaryMetadataPath,
|
||||
JsonSerializer.SerializeToUtf8Bytes(
|
||||
metadata,
|
||||
new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
if (metadataPath is not null && temporaryMetadataPath is not null)
|
||||
File.Move(temporaryMetadataPath, metadataPath, overwrite: true);
|
||||
File.Move(temporaryPath, path, overwrite: true);
|
||||
|
||||
_status[name] = new CaptureStatus(CaptureState.Complete);
|
||||
|
|
@ -93,6 +120,9 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
TryDelete(Path.Combine(_directory, name + ".png.tmp"));
|
||||
TryDelete(Path.Combine(_directory, name + ".metadata.json.tmp"));
|
||||
TryDelete(Path.Combine(_directory, name + ".metadata.json"));
|
||||
string message = $"screenshot '{name}' failed: {exception.Message}";
|
||||
_status[name] = new CaptureStatus(CaptureState.Failed, message);
|
||||
_log($"[world-gate] screenshot-failed name={name} error={exception.Message}");
|
||||
|
|
@ -100,6 +130,22 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception error) when (error is IOException
|
||||
or UnauthorizedAccessException
|
||||
or ArgumentException
|
||||
or NotSupportedException)
|
||||
{
|
||||
// Preserve the primary capture error. The next artifact directory
|
||||
// teardown reports any file that could not be cleaned.
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] FlipRows(byte[] pixels, int width, int height)
|
||||
{
|
||||
int stride = checked(width * 4);
|
||||
|
|
@ -249,3 +295,9 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed record FrameScreenshotMetadata(
|
||||
int SchemaVersion,
|
||||
int Width,
|
||||
int Height,
|
||||
RenderPackDiagnosticsSnapshot RenderPack);
|
||||
|
|
|
|||
|
|
@ -240,6 +240,22 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
private readonly Func<RuntimeWorldTransitOwnershipSnapshot>
|
||||
_getTransitOwnership;
|
||||
private readonly Func<int> _getPortalMaterializationCount;
|
||||
private readonly Func<int> _getRenderPackPerformanceSampleCount;
|
||||
private readonly Func<bool> _getRenderPackFailedToRetail;
|
||||
private readonly Func<RetailUiAutomationRenderPackStatus>
|
||||
_getRenderPackStatus;
|
||||
private readonly Func<string, (bool Succeeded, string Error)>
|
||||
_selectRenderPack;
|
||||
private readonly Func<(bool Succeeded, string Error)>?
|
||||
_disableRenderPack;
|
||||
private readonly Func<(bool Succeeded, string Error)>?
|
||||
_reenableRenderPack;
|
||||
private readonly Func<(int Width, int Height)> _getFramebufferSize;
|
||||
private readonly Func<int, int, (bool Succeeded, string Error)>
|
||||
_resizeFramebuffer;
|
||||
private readonly Func<(bool Succeeded, string Error)>
|
||||
_resetRenderPackPerformance;
|
||||
private readonly Action? _requestClientClose;
|
||||
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
||||
_captureResources;
|
||||
private readonly FrameScreenshotController _screenshots;
|
||||
|
|
@ -248,6 +264,7 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
private readonly object _requestOwner = new();
|
||||
private readonly object _sync = new();
|
||||
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
|
||||
private string? _lastEnabledRenderPackPreset;
|
||||
private int _sequence;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -260,7 +277,17 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
||||
FrameScreenshotController screenshots,
|
||||
string artifactDirectory,
|
||||
Action<string>? log = null)
|
||||
Action<string>? log = null,
|
||||
Func<int>? getRenderPackPerformanceSampleCount = null,
|
||||
Func<(bool Succeeded, string Error)>? resetRenderPackPerformance = null,
|
||||
Func<bool>? getRenderPackFailedToRetail = null,
|
||||
Func<RetailUiAutomationRenderPackStatus>? getRenderPackStatus = null,
|
||||
Func<string, (bool Succeeded, string Error)>? selectRenderPack = null,
|
||||
Func<(bool Succeeded, string Error)>? disableRenderPack = null,
|
||||
Func<(bool Succeeded, string Error)>? reenableRenderPack = null,
|
||||
Func<(int Width, int Height)>? getFramebufferSize = null,
|
||||
Func<int, int, (bool Succeeded, string Error)>? resizeFramebuffer = null,
|
||||
Action? requestClientClose = null)
|
||||
{
|
||||
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
|
||||
_getEnvironmentOwnership = getEnvironmentOwnership
|
||||
|
|
@ -270,6 +297,21 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
?? throw new ArgumentNullException(nameof(getTransitOwnership));
|
||||
_getPortalMaterializationCount = getPortalMaterializationCount
|
||||
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
|
||||
_getRenderPackPerformanceSampleCount =
|
||||
getRenderPackPerformanceSampleCount ?? (() => 0);
|
||||
_getRenderPackFailedToRetail = getRenderPackFailedToRetail ?? (() => false);
|
||||
_getRenderPackStatus = getRenderPackStatus
|
||||
?? (() => RetailUiAutomationRenderPackStatus.Retail);
|
||||
_selectRenderPack = selectRenderPack
|
||||
?? (_ => (false, "render-pack selection automation is unavailable"));
|
||||
_disableRenderPack = disableRenderPack;
|
||||
_reenableRenderPack = reenableRenderPack;
|
||||
_getFramebufferSize = getFramebufferSize ?? (() => (0, 0));
|
||||
_resizeFramebuffer = resizeFramebuffer
|
||||
?? ((_, _) => (false, "framebuffer resize automation is unavailable"));
|
||||
_resetRenderPackPerformance = resetRenderPackPerformance
|
||||
?? (() => (false, "render-pack performance automation is unavailable"));
|
||||
_requestClientClose = requestClientClose;
|
||||
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
|
||||
_screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots));
|
||||
_artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory)
|
||||
|
|
@ -281,6 +323,113 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
public bool IsWorldReady => _getReveal().IsReady;
|
||||
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
|
||||
public int PortalMaterializationCount => _getPortalMaterializationCount();
|
||||
public int RenderPackPerformanceSampleCount =>
|
||||
_getRenderPackPerformanceSampleCount();
|
||||
public bool RenderPackFailedToRetail => _getRenderPackFailedToRetail();
|
||||
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
|
||||
_getRenderPackStatus();
|
||||
public int FramebufferWidth => _getFramebufferSize().Width;
|
||||
public int FramebufferHeight => _getFramebufferSize().Height;
|
||||
|
||||
public bool TrySelectRenderPack(string presetId, out string error)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(presetId);
|
||||
string normalized = presetId.ToLowerInvariant();
|
||||
if (normalized == "off")
|
||||
normalized = "retail";
|
||||
if (normalized is not ("retail" or "low" or "medium" or "high" or "auto"))
|
||||
{
|
||||
error = $"unknown render-pack preset '{presetId}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
(bool succeeded, string selectionError) = _selectRenderPack(normalized);
|
||||
if (succeeded && normalized != "retail")
|
||||
_lastEnabledRenderPackPreset = normalized;
|
||||
error = selectionError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryDisableRenderPack(out string error)
|
||||
{
|
||||
if (_disableRenderPack is not null)
|
||||
{
|
||||
(bool succeeded, string disableError) = _disableRenderPack();
|
||||
error = disableError;
|
||||
return succeeded;
|
||||
}
|
||||
RetailUiAutomationRenderPackStatus current = RenderPackStatus;
|
||||
if (current.State == RetailUiAutomationRenderPackState.Active
|
||||
&& !string.Equals(current.PackId, "retail", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_lastEnabledRenderPackPreset = current.PresetId;
|
||||
}
|
||||
return TrySelectRenderPack("retail", out error);
|
||||
}
|
||||
|
||||
public bool TryReenableRenderPack(out string error)
|
||||
{
|
||||
if (_reenableRenderPack is not null)
|
||||
{
|
||||
(bool succeeded, string reenableError) = _reenableRenderPack();
|
||||
error = reenableError;
|
||||
return succeeded;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_lastEnabledRenderPackPreset))
|
||||
{
|
||||
error = "render-pack re-enable requires a prior active enhanced selection";
|
||||
return false;
|
||||
}
|
||||
return TrySelectRenderPack(_lastEnabledRenderPackPreset, out error);
|
||||
}
|
||||
|
||||
public bool TryResizeFramebuffer(int width, int height, out string error)
|
||||
{
|
||||
if (width < 320 || height < 240 || width > 8192 || height > 8192)
|
||||
{
|
||||
error = "automation framebuffer size must be within 320x240 and 8192x8192";
|
||||
return false;
|
||||
}
|
||||
(bool succeeded, string resizeError) = _resizeFramebuffer(width, height);
|
||||
error = resizeError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryResetRenderPackPerformance(out string error)
|
||||
{
|
||||
// A terminal fallback owns no enhanced evidence. Treat reset as an
|
||||
// idempotent no-op so a reset/wait/screenshot automation sequence can
|
||||
// report the unavailable preset instead of stopping before capture.
|
||||
if (RenderPackFailedToRetail)
|
||||
{
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
(bool succeeded, string resetError) = _resetRenderPackPerformance();
|
||||
error = resetError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryRequestClientClose(out string error)
|
||||
{
|
||||
if (_requestClientClose is null)
|
||||
{
|
||||
error = "client-close automation is unavailable";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_requestClientClose();
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = $"client-close automation failed: {exception.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRequestCheckpoint(
|
||||
string name,
|
||||
|
|
|
|||
171
src/AcDream.App/Plugins/BufferedRenderPackRegistry.cs
Normal file
171
src/AcDream.App/Plugins/BufferedRenderPackRegistry.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-device render-pack discovery buffer. Registration only retains the
|
||||
/// immutable declaration and lazy asset source; it deliberately never calls
|
||||
/// <see cref="IRenderPackAssets.OpenRead"/> or touches the renderer. This lets
|
||||
/// plugins register before the window/GPU exists without weakening the retail
|
||||
/// no-op contract.
|
||||
/// </summary>
|
||||
internal sealed class BufferedRenderPackRegistry : IRenderPackRegistry, IDisposable
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly Dictionary<string, Registration> _registrations =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private long _revision;
|
||||
private long _nextRegistrationId;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Monotonic catalog generation. Consumers use <see cref="Changed"/> to
|
||||
/// invalidate their cached view and consume the new snapshot at a safe
|
||||
/// frame/UI boundary; no renderer path polls the registry per frame.
|
||||
/// </summary>
|
||||
internal long Revision
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _revision;
|
||||
}
|
||||
}
|
||||
|
||||
internal event Action<long>? Changed;
|
||||
|
||||
internal IReadOnlyList<BufferedRenderPackRegistration> Snapshot()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _registrations.Values
|
||||
.OrderBy(static value => value.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(static value => new BufferedRenderPackRegistration(
|
||||
value.Descriptor,
|
||||
value.Assets,
|
||||
value.RegistrationId))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Register(
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
|
||||
Registration registration;
|
||||
long revision;
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_registrations.ContainsKey(descriptor.Id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A render pack with id '{descriptor.Id}' is already registered.");
|
||||
}
|
||||
|
||||
registration = new Registration(
|
||||
this,
|
||||
descriptor,
|
||||
assets,
|
||||
checked(++_nextRegistrationId));
|
||||
_registrations.Add(descriptor.Id, registration);
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
|
||||
PublishChanged(revision);
|
||||
return registration;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Registration[] registrations;
|
||||
long? revision = null;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
registrations = _registrations.Values.ToArray();
|
||||
_registrations.Clear();
|
||||
if (registrations.Length != 0)
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
|
||||
foreach (Registration registration in registrations)
|
||||
registration.WithdrawFromOwner();
|
||||
if (revision is { } changedRevision)
|
||||
PublishChanged(changedRevision);
|
||||
}
|
||||
|
||||
private void Withdraw(Registration registration)
|
||||
{
|
||||
long? revision = null;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_registrations.TryGetValue(
|
||||
registration.Descriptor.Id,
|
||||
out Registration? active)
|
||||
&& ReferenceEquals(active, registration))
|
||||
{
|
||||
_registrations.Remove(registration.Descriptor.Id);
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
}
|
||||
|
||||
if (revision is { } changedRevision)
|
||||
PublishChanged(changedRevision);
|
||||
}
|
||||
|
||||
private void PublishChanged(long revision)
|
||||
{
|
||||
Delegate[] subscribers = Changed?.GetInvocationList() ?? [];
|
||||
foreach (Delegate subscriber in subscribers)
|
||||
{
|
||||
try { ((Action<long>)subscriber)(revision); }
|
||||
catch
|
||||
{
|
||||
// Registration ownership must not be corrupted by a UI or
|
||||
// controller observer. The next explicit snapshot still sees
|
||||
// the authoritative revision and contents.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Registration : IDisposable
|
||||
{
|
||||
private BufferedRenderPackRegistry? _owner;
|
||||
|
||||
internal Registration(
|
||||
BufferedRenderPackRegistry owner,
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
long registrationId)
|
||||
{
|
||||
_owner = owner;
|
||||
Descriptor = descriptor;
|
||||
Assets = assets;
|
||||
RegistrationId = registrationId;
|
||||
}
|
||||
|
||||
internal RenderPackDescriptor Descriptor { get; }
|
||||
|
||||
internal IRenderPackAssets Assets { get; }
|
||||
|
||||
internal long RegistrationId { get; }
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Withdraw(this);
|
||||
|
||||
internal void WithdrawFromOwner() =>
|
||||
Interlocked.Exchange(ref _owner, null);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record BufferedRenderPackRegistration(
|
||||
RenderPackDescriptor Descriptor,
|
||||
IRenderPackAssets Assets,
|
||||
long RegistrationId);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.Platform;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
|
@ -44,7 +45,8 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
IReadOnlyList<string>? allowList,
|
||||
string sessionId,
|
||||
IPluginHost host,
|
||||
SessionStatusWriter statusWriter)
|
||||
SessionStatusWriter statusWriter,
|
||||
IRenderPackRegistry? renderPacks = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
||||
|
|
@ -53,7 +55,11 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
|
||||
var plugins = new PluginSession(
|
||||
host,
|
||||
status => Report(statusWriter, sessionId, status));
|
||||
status => Report(statusWriter, sessionId, status),
|
||||
renderPacks,
|
||||
renderPacks is null
|
||||
? [PluginKind.Gameplay]
|
||||
: [PluginKind.Gameplay, PluginKind.RenderPack]);
|
||||
return new GraphicalPluginSession(
|
||||
plugins,
|
||||
[
|
||||
|
|
|
|||
|
|
@ -149,6 +149,15 @@ if (runtimeOptions.DevTools)
|
|||
var worldGameState = new AcDream.Core.Plugins.WorldGameState();
|
||||
var worldEvents = new AcDream.Core.Plugins.WorldEvents();
|
||||
var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry();
|
||||
using var renderPackRegistry = new AcDream.App.Plugins.BufferedRenderPackRegistry();
|
||||
using IDisposable atmosphericPackRegistration = renderPackRegistry.Register(
|
||||
AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.Descriptor,
|
||||
AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.CreateAssets(
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Rendering",
|
||||
"Shaders",
|
||||
"spv")));
|
||||
// Constructed here and handed to both sides: GameWindow binds it to the live
|
||||
// session's Runtime owners, the plugin host exposes it to plugins.
|
||||
using var automation = new AcDream.App.Plugins.AppAutomationSurface();
|
||||
|
|
@ -158,7 +167,8 @@ using var window = new GameWindow(
|
|||
worldEvents,
|
||||
uiRegistry,
|
||||
graphicalPlatform,
|
||||
automation);
|
||||
automation,
|
||||
renderPackRegistry);
|
||||
var host = new AppPluginHost(
|
||||
new SerilogAdapter(Log.Logger),
|
||||
worldGameState,
|
||||
|
|
@ -171,7 +181,8 @@ GraphicalPluginSession pluginSession = GraphicalPluginSession.Create(
|
|||
runtimeOptions.Plugins,
|
||||
runtimeOptions.SessionId ?? "app",
|
||||
host,
|
||||
window.StatusWriter);
|
||||
window.StatusWriter,
|
||||
renderPackRegistry);
|
||||
window.StartPluginHosting(pluginSession);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -194,14 +194,14 @@ internal sealed class RhiCompositeTextureArrayBackend : ICompositeTextureArrayBa
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL backend reads <c>GL_MAX_ARRAY_TEXTURE_LAYERS</c>. The pinned
|
||||
/// <see cref="Gpu.GpuCapabilityRecord"/> has no array-layer field and §3.3 is
|
||||
/// frozen, so this reports Vulkan's guaranteed <c>maxImageArrayLayers</c>
|
||||
/// minimum of 256. That is not a limitation in practice:
|
||||
/// The selected Vulkan adapter's probed <c>maxImageArrayLayers</c>. This is
|
||||
/// normally far above the cache's own bound:
|
||||
/// <see cref="CompositeTextureArrayCache.MaximumLayersPerArray"/> caps every
|
||||
/// array at 64, so the true device limit is never the binding constraint.
|
||||
/// </summary>
|
||||
public int MaximumArrayLayers => 256;
|
||||
public int MaximumArrayLayers => checked((int)Math.Min(
|
||||
_device.Capabilities.MaxImageArrayLayers,
|
||||
(uint)int.MaxValue));
|
||||
|
||||
public CompositeTextureArrayResource Create(int width, int height, int capacity)
|
||||
{
|
||||
|
|
|
|||
286
src/AcDream.App/Rendering/DirectionalShadowCascadeFitter.cs
Normal file
286
src/AcDream.App/Rendering/DirectionalShadowCascadeFitter.cs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalShadowCascadeFitInput(
|
||||
Matrix4x4 CameraView,
|
||||
Matrix4x4 CameraProjection,
|
||||
Vector3 SurfaceToLightDirection,
|
||||
DirectionalShadowQuality Quality,
|
||||
float CameraNearMeters = 0.1f,
|
||||
float PracticalSplitLambda = 0.65f,
|
||||
float CasterDepthPaddingMeters = 48f,
|
||||
float ResidentMaximumReachMeters = float.PositiveInfinity);
|
||||
|
||||
internal readonly record struct DirectionalShadowCascade(
|
||||
int Index,
|
||||
float SplitNearMeters,
|
||||
float SplitFarMeters,
|
||||
Matrix4x4 LightView,
|
||||
Matrix4x4 LightProjection,
|
||||
Matrix4x4 WorldToShadowClip,
|
||||
Vector2 StabilizedLightSpaceCenter,
|
||||
float HalfExtentMeters,
|
||||
float TexelWorldSize,
|
||||
float CasterDepthPaddingMeters,
|
||||
DirectionalShadowWorldBias Bias);
|
||||
|
||||
/// <summary>
|
||||
/// Pure camera-relative cascade fitting. It receives no scene/PView callback,
|
||||
/// so fitting N cascades cannot trigger N CPU visibility traversals.
|
||||
/// </summary>
|
||||
internal static class DirectionalShadowCascadeFitter
|
||||
{
|
||||
private const float RadiusQuantizationMeters = 1f / 16f;
|
||||
|
||||
public static int Fit(
|
||||
in DirectionalShadowCascadeFitInput input,
|
||||
Span<DirectionalShadowCascade> destination)
|
||||
{
|
||||
Validate(in input, destination.Length);
|
||||
if (!Matrix4x4.Invert(input.CameraView, out Matrix4x4 inverseView))
|
||||
throw new ArgumentException("Camera view matrix is not invertible.", nameof(input));
|
||||
if (!Matrix4x4.Invert(input.CameraProjection, out Matrix4x4 inverseProjection))
|
||||
throw new ArgumentException("Camera projection matrix is not invertible.", nameof(input));
|
||||
|
||||
Vector3 lightDirection = Vector3.Normalize(input.SurfaceToLightDirection);
|
||||
float maximumReach = MathF.Min(
|
||||
input.Quality.MaximumReachMeters,
|
||||
input.ResidentMaximumReachMeters);
|
||||
if (maximumReach <= input.CameraNearMeters)
|
||||
return 0;
|
||||
float splitNear = input.CameraNearMeters;
|
||||
Span<Vector3> corners = stackalloc Vector3[8];
|
||||
for (int cascadeIndex = 0;
|
||||
cascadeIndex < input.Quality.CascadeCount;
|
||||
cascadeIndex++)
|
||||
{
|
||||
float splitFar = PracticalSplit(
|
||||
input.CameraNearMeters,
|
||||
maximumReach,
|
||||
cascadeIndex + 1,
|
||||
input.Quality.CascadeCount,
|
||||
input.PracticalSplitLambda);
|
||||
BuildFrustumSliceCorners(
|
||||
inverseView,
|
||||
inverseProjection,
|
||||
splitNear,
|
||||
splitFar,
|
||||
corners);
|
||||
destination[cascadeIndex] = FitCascade(
|
||||
cascadeIndex,
|
||||
splitNear,
|
||||
splitFar,
|
||||
corners,
|
||||
lightDirection,
|
||||
input.Quality.MapResolution,
|
||||
input.CasterDepthPaddingMeters,
|
||||
input.Quality.BiasPolicy);
|
||||
splitNear = splitFar;
|
||||
}
|
||||
|
||||
return input.Quality.CascadeCount;
|
||||
}
|
||||
|
||||
internal static float PracticalSplit(
|
||||
float nearMeters,
|
||||
float farMeters,
|
||||
int splitIndex,
|
||||
int splitCount,
|
||||
float lambda)
|
||||
{
|
||||
if (!float.IsFinite(nearMeters)
|
||||
|| !float.IsFinite(farMeters)
|
||||
|| nearMeters <= 0f
|
||||
|| farMeters <= nearMeters)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(farMeters));
|
||||
}
|
||||
if (splitCount <= 0 || splitIndex <= 0 || splitIndex > splitCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(splitIndex));
|
||||
if (!float.IsFinite(lambda) || lambda < 0f || lambda > 1f)
|
||||
throw new ArgumentOutOfRangeException(nameof(lambda));
|
||||
|
||||
float fraction = (float)splitIndex / splitCount;
|
||||
float logarithmic = nearMeters * MathF.Pow(farMeters / nearMeters, fraction);
|
||||
float uniform = nearMeters + (farMeters - nearMeters) * fraction;
|
||||
return lambda * logarithmic + (1f - lambda) * uniform;
|
||||
}
|
||||
|
||||
private static DirectionalShadowCascade FitCascade(
|
||||
int index,
|
||||
float splitNear,
|
||||
float splitFar,
|
||||
ReadOnlySpan<Vector3> corners,
|
||||
Vector3 surfaceToLight,
|
||||
int mapResolution,
|
||||
float depthPadding,
|
||||
in DirectionalShadowBiasPolicy biasPolicy)
|
||||
{
|
||||
Vector3 center = Vector3.Zero;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
center += corners[i];
|
||||
center /= corners.Length;
|
||||
|
||||
float radius = 0f;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
radius = MathF.Max(radius, Vector3.Distance(center, corners[i]));
|
||||
radius = MathF.Ceiling(radius / RadiusQuantizationMeters)
|
||||
* RadiusQuantizationMeters;
|
||||
radius = MathF.Max(radius, RadiusQuantizationMeters);
|
||||
|
||||
Vector3 up = StableLightUp(surfaceToLight);
|
||||
Matrix4x4 lightRotation = Matrix4x4.CreateLookAt(
|
||||
Vector3.Zero,
|
||||
-surfaceToLight,
|
||||
up);
|
||||
|
||||
Vector3 lightCenter = Vector3.Transform(center, lightRotation);
|
||||
float texelWorldSize = (2f * radius) / mapResolution;
|
||||
float snappedX = SnapToTexel(lightCenter.X, texelWorldSize);
|
||||
float snappedY = SnapToTexel(lightCenter.Y, texelWorldSize);
|
||||
|
||||
float minZ = float.PositiveInfinity;
|
||||
float maxZ = float.NegativeInfinity;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
{
|
||||
float z = Vector3.Transform(corners[i], lightRotation).Z;
|
||||
minZ = MathF.Min(minZ, z);
|
||||
maxZ = MathF.Max(maxZ, z);
|
||||
}
|
||||
|
||||
// Move the light eye toward the selected celestial source. The
|
||||
// receiver slice then lies
|
||||
// between depthPadding and span+depthPadding metres in front of it,
|
||||
// while the far extension admits casters behind the slice as well.
|
||||
float eyeAxis = maxZ + depthPadding;
|
||||
Vector3 eye = surfaceToLight * eyeAxis;
|
||||
Matrix4x4 lightView = Matrix4x4.CreateLookAt(
|
||||
eye,
|
||||
eye - surfaceToLight,
|
||||
up);
|
||||
float nearPlane = 0.1f;
|
||||
float farPlane = MathF.Max(
|
||||
nearPlane + 0.1f,
|
||||
(maxZ - minZ) + 2f * depthPadding);
|
||||
Matrix4x4 lightProjection = Matrix4x4.CreateOrthographicOffCenter(
|
||||
snappedX - radius,
|
||||
snappedX + radius,
|
||||
snappedY - radius,
|
||||
snappedY + radius,
|
||||
nearPlane,
|
||||
farPlane);
|
||||
|
||||
return new DirectionalShadowCascade(
|
||||
index,
|
||||
splitNear,
|
||||
splitFar,
|
||||
lightView,
|
||||
lightProjection,
|
||||
lightView * lightProjection,
|
||||
new Vector2(snappedX, snappedY),
|
||||
radius,
|
||||
texelWorldSize,
|
||||
depthPadding,
|
||||
biasPolicy.Resolve(texelWorldSize));
|
||||
}
|
||||
|
||||
private static void BuildFrustumSliceCorners(
|
||||
Matrix4x4 inverseView,
|
||||
Matrix4x4 inverseProjection,
|
||||
float nearMeters,
|
||||
float farMeters,
|
||||
Span<Vector3> destination)
|
||||
{
|
||||
int cursor = 0;
|
||||
for (int depthIndex = 0; depthIndex < 2; depthIndex++)
|
||||
{
|
||||
float distance = depthIndex == 0 ? nearMeters : farMeters;
|
||||
for (int yIndex = 0; yIndex < 2; yIndex++)
|
||||
{
|
||||
float y = yIndex == 0 ? -1f : 1f;
|
||||
for (int xIndex = 0; xIndex < 2; xIndex++)
|
||||
{
|
||||
float x = xIndex == 0 ? -1f : 1f;
|
||||
Vector4 viewCorner = Vector4.Transform(
|
||||
new Vector4(x, y, 1f, 1f),
|
||||
inverseProjection);
|
||||
if (MathF.Abs(viewCorner.W) <= 1e-6f)
|
||||
throw new ArgumentException("Camera projection produced a corner at infinity.");
|
||||
Vector3 view = new(
|
||||
viewCorner.X / viewCorner.W,
|
||||
viewCorner.Y / viewCorner.W,
|
||||
viewCorner.Z / viewCorner.W);
|
||||
float viewDepth = MathF.Abs(view.Z);
|
||||
if (viewDepth <= 1e-6f)
|
||||
throw new ArgumentException("Camera projection produced zero view depth.");
|
||||
view *= distance / viewDepth;
|
||||
destination[cursor++] = Vector3.Transform(view, inverseView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float SnapToTexel(float value, float texelWorldSize) =>
|
||||
MathF.Round(value / texelWorldSize, MidpointRounding.AwayFromZero)
|
||||
* texelWorldSize;
|
||||
|
||||
/// <summary>
|
||||
/// Uses Duff's numerically stable revision of Frisvad's orthonormal basis.
|
||||
/// The selected celestial source occupies the accepted upper hemisphere,
|
||||
/// where this basis varies continuously through the exact zenith. The old
|
||||
/// 0.95 dot-product branch rotated the cascade basis abruptly, while
|
||||
/// projected world-up merely moved that discontinuity to exact zenith.
|
||||
/// </summary>
|
||||
internal static Vector3 StableLightUp(Vector3 surfaceToLight)
|
||||
{
|
||||
surfaceToLight = Vector3.Normalize(surfaceToLight);
|
||||
float sign = MathF.CopySign(1f, surfaceToLight.Z);
|
||||
float a = -1f / (sign + surfaceToLight.Z);
|
||||
float b = surfaceToLight.X * surfaceToLight.Y * a;
|
||||
return Vector3.Normalize(new Vector3(
|
||||
b,
|
||||
sign + surfaceToLight.Y * surfaceToLight.Y * a,
|
||||
-surfaceToLight.Y));
|
||||
}
|
||||
|
||||
private static void Validate(
|
||||
in DirectionalShadowCascadeFitInput input,
|
||||
int destinationLength)
|
||||
{
|
||||
DirectionalShadowQuality quality = input.Quality;
|
||||
if (quality.CascadeCount <= 0 || quality.CascadeCount > 4)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Cascade count must be in [1,4].");
|
||||
if (destinationLength < quality.CascadeCount)
|
||||
throw new ArgumentException("Destination cannot hold every configured cascade.");
|
||||
if (quality.MapResolution <= 0
|
||||
|| !float.IsFinite(quality.MaximumReachMeters)
|
||||
|| quality.MaximumReachMeters <= input.CameraNearMeters)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Shadow quality dimensions are invalid.");
|
||||
}
|
||||
if (!float.IsFinite(input.CameraNearMeters) || input.CameraNearMeters <= 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Camera near distance must be positive.");
|
||||
if (float.IsNaN(input.ResidentMaximumReachMeters)
|
||||
|| input.ResidentMaximumReachMeters < 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(input),
|
||||
"Resident shadow reach must be nonnegative or positive infinity.");
|
||||
}
|
||||
if (!float.IsFinite(input.PracticalSplitLambda)
|
||||
|| input.PracticalSplitLambda < 0f
|
||||
|| input.PracticalSplitLambda > 1f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Split lambda must be in [0,1].");
|
||||
}
|
||||
if (!float.IsFinite(input.CasterDepthPaddingMeters)
|
||||
|| input.CasterDepthPaddingMeters <= 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Caster depth padding must be positive.");
|
||||
}
|
||||
float lightLength = input.SurfaceToLightDirection.Length();
|
||||
if (!float.IsFinite(lightLength) || lightLength <= 1e-6f)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Light direction must be finite and nonzero.");
|
||||
}
|
||||
}
|
||||
362
src/AcDream.App/Rendering/DirectionalShadowQuality.cs
Normal file
362
src/AcDream.App/Rendering/DirectionalShadowQuality.cs
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// User-visible quality rows for Dereth's selected celestial directional shadows. Presets may
|
||||
/// reduce count, resolution, reach, and filtering cost; they never remove a
|
||||
/// headline caster class.
|
||||
/// </summary>
|
||||
internal enum DirectionalShadowPreset : byte
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum DirectionalShadowSemantics : ushort
|
||||
{
|
||||
None = 0,
|
||||
Terrain = 1 << 0,
|
||||
TreesAndOutdoorStatics = 1 << 1,
|
||||
Buildings = 1 << 2,
|
||||
Players = 1 << 3,
|
||||
Monsters = 1 << 4,
|
||||
AnimatedTransforms = 1 << 5,
|
||||
AlphaCutoutCasters = 1 << 6,
|
||||
|
||||
Headline = Terrain
|
||||
| TreesAndOutdoorStatics
|
||||
| Buildings
|
||||
| Players
|
||||
| Monsters
|
||||
| AnimatedTransforms
|
||||
| AlphaCutoutCasters,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts shadow-map texel footprint into receiver-side offsets expressed in
|
||||
/// metres. No term is an NDC constant: the projection may change without
|
||||
/// silently changing the amount of world geometry displaced.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowBiasPolicy(
|
||||
float ConstantTexels,
|
||||
float SlopeTexels,
|
||||
float NormalTexels,
|
||||
float MinimumMeters,
|
||||
float MaximumMeters)
|
||||
{
|
||||
public DirectionalShadowWorldBias Resolve(float texelWorldSize)
|
||||
{
|
||||
if (!float.IsFinite(texelWorldSize) || texelWorldSize <= 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(texelWorldSize));
|
||||
if (!float.IsFinite(MinimumMeters)
|
||||
|| !float.IsFinite(MaximumMeters)
|
||||
|| MinimumMeters < 0f
|
||||
|| MaximumMeters < MinimumMeters)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow bias bounds must be finite, non-negative, and ordered.");
|
||||
}
|
||||
|
||||
var minimumMeters = MinimumMeters;
|
||||
var maximumMeters = MaximumMeters;
|
||||
return new DirectionalShadowWorldBias(
|
||||
ConstantDepthMeters: Math.Clamp(
|
||||
ConstantTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters),
|
||||
SlopeDepthMeters: Math.Clamp(
|
||||
SlopeTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters),
|
||||
NormalOffsetMeters: Math.Clamp(
|
||||
NormalTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters));
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowWorldBias(
|
||||
float ConstantDepthMeters,
|
||||
float SlopeDepthMeters,
|
||||
float NormalOffsetMeters);
|
||||
|
||||
internal readonly record struct DirectionalShadowQuality(
|
||||
DirectionalShadowPreset Preset,
|
||||
int CascadeCount,
|
||||
int MapResolution,
|
||||
float MaximumReachMeters,
|
||||
int PcfRadiusTexels,
|
||||
long ApproximateDepthMapBytes,
|
||||
double IncrementalGpuP50BudgetMilliseconds,
|
||||
double IncrementalGpuP99BudgetMilliseconds,
|
||||
double IncrementalCpuP50BudgetMilliseconds,
|
||||
double IncrementalCpuP99BudgetMilliseconds,
|
||||
long PackResidentGpuByteBudget,
|
||||
DirectionalShadowSemantics Semantics,
|
||||
DirectionalShadowBiasPolicy BiasPolicy)
|
||||
{
|
||||
private const long MiB = 1024L * 1024L;
|
||||
|
||||
public static DirectionalShadowQuality For(DirectionalShadowPreset preset) =>
|
||||
preset switch
|
||||
{
|
||||
DirectionalShadowPreset.Low => Create(
|
||||
preset,
|
||||
cascades: 2,
|
||||
// The physical integrated-GPU row funds Low's cheaper
|
||||
// quarter-resolution separable post path by reducing only
|
||||
// texel density. Both cascades and every semantic caster
|
||||
// class remain present.
|
||||
resolution: 768,
|
||||
reachMeters: 72f,
|
||||
pcfRadius: 0,
|
||||
gpuP50: 2.0,
|
||||
gpuP99: 3.0,
|
||||
cpuP50: 0.15,
|
||||
cpuP99: 0.50,
|
||||
residentBudget: 64L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.45f, 1.25f, 1.0f, 0.001f, 0.35f)),
|
||||
DirectionalShadowPreset.Medium => Create(
|
||||
preset,
|
||||
cascades: 3,
|
||||
resolution: 1536,
|
||||
reachMeters: 144f,
|
||||
pcfRadius: 1,
|
||||
gpuP50: 3.25,
|
||||
gpuP99: 4.50,
|
||||
cpuP50: 0.25,
|
||||
cpuP99: 0.75,
|
||||
residentBudget: 128L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.40f, 1.15f, 0.9f, 0.001f, 0.30f)),
|
||||
DirectionalShadowPreset.High => Create(
|
||||
preset,
|
||||
cascades: 4,
|
||||
resolution: 2048,
|
||||
reachMeters: 240f,
|
||||
pcfRadius: 2,
|
||||
gpuP50: 4.50,
|
||||
gpuP99: 6.00,
|
||||
cpuP50: 0.35,
|
||||
cpuP99: 1.00,
|
||||
residentBudget: 256L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.35f, 1.0f, 0.8f, 0.001f, 0.25f)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(preset), preset, null),
|
||||
};
|
||||
|
||||
private static DirectionalShadowQuality Create(
|
||||
DirectionalShadowPreset preset,
|
||||
int cascades,
|
||||
int resolution,
|
||||
float reachMeters,
|
||||
int pcfRadius,
|
||||
double gpuP50,
|
||||
double gpuP99,
|
||||
double cpuP50,
|
||||
double cpuP99,
|
||||
long residentBudget,
|
||||
DirectionalShadowBiasPolicy bias) =>
|
||||
new(
|
||||
preset,
|
||||
cascades,
|
||||
resolution,
|
||||
reachMeters,
|
||||
pcfRadius,
|
||||
checked((long)cascades * resolution * resolution * sizeof(float)),
|
||||
gpuP50,
|
||||
gpuP99,
|
||||
cpuP50,
|
||||
cpuP99,
|
||||
residentBudget,
|
||||
DirectionalShadowSemantics.Headline,
|
||||
bias);
|
||||
}
|
||||
|
||||
internal enum DirectionalShadowGateReason : byte
|
||||
{
|
||||
Enabled,
|
||||
PackDisabled,
|
||||
PortalOrLoginCover,
|
||||
Indoor,
|
||||
NoVisibleCelestial,
|
||||
SelectedLightBelowHorizon,
|
||||
SelectedLightHasNoEnergy,
|
||||
AtmosphereSuppressed,
|
||||
ResidentWindowUnavailable,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visible policy owned by the selected atmospheric pack. AC continues to own
|
||||
/// the sky and weather inputs; these values only map them to enhancement
|
||||
/// strength and softness.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowAtmospherePolicy(
|
||||
float MinimumLightElevationSin,
|
||||
float FullStrengthLightElevationSin,
|
||||
float ClearStrength,
|
||||
float OvercastStrength,
|
||||
float RainStrength,
|
||||
float SnowStrength,
|
||||
float StormStrength,
|
||||
float ClearSoftness,
|
||||
float OvercastSoftness,
|
||||
float RainSoftness,
|
||||
float SnowSoftness,
|
||||
float StormSoftness)
|
||||
{
|
||||
public static DirectionalShadowAtmospherePolicy BuiltIn { get; } = new(
|
||||
MinimumLightElevationSin: MathF.Sin(MathF.PI / 180f),
|
||||
FullStrengthLightElevationSin: MathF.Sin(12f * MathF.PI / 180f),
|
||||
ClearStrength: 1.0f,
|
||||
OvercastStrength: 0.65f,
|
||||
RainStrength: 0.45f,
|
||||
SnowStrength: 0.60f,
|
||||
StormStrength: 0.25f,
|
||||
ClearSoftness: 1.0f,
|
||||
OvercastSoftness: 1.5f,
|
||||
RainSoftness: 1.8f,
|
||||
SnowSoftness: 1.6f,
|
||||
StormSoftness: 2.0f);
|
||||
|
||||
public float StrengthFor(WeatherKind weather) => weather switch
|
||||
{
|
||||
WeatherKind.Clear => ClearStrength,
|
||||
WeatherKind.Overcast => OvercastStrength,
|
||||
WeatherKind.Rain => RainStrength,
|
||||
WeatherKind.Snow => SnowStrength,
|
||||
WeatherKind.Storm => StormStrength,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(weather), weather, null),
|
||||
};
|
||||
|
||||
public float SoftnessFor(WeatherKind weather) => weather switch
|
||||
{
|
||||
WeatherKind.Clear => ClearSoftness,
|
||||
WeatherKind.Overcast => OvercastSoftness,
|
||||
WeatherKind.Rain => RainSoftness,
|
||||
WeatherKind.Snow => SnowSoftness,
|
||||
WeatherKind.Storm => StormSoftness,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(weather), weather, null),
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowEnvironmentInput(
|
||||
bool PackEnabled,
|
||||
bool PortalOrLoginCoverVisible,
|
||||
bool PlayerInsideCell,
|
||||
AuthoredCelestialShadowSource Source,
|
||||
AtmosphereSnapshot Atmosphere,
|
||||
float ActiveDayGroupMultiplier = 1f);
|
||||
|
||||
internal readonly record struct DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason Reason,
|
||||
System.Numerics.Vector3 SurfaceToLightDirection,
|
||||
float LightElevationSin,
|
||||
float Strength,
|
||||
float SoftnessMultiplier,
|
||||
AuthoredCelestialShadowSourceKind SourceKind =
|
||||
AuthoredCelestialShadowSourceKind.None,
|
||||
int SourceObjectIndex = -1,
|
||||
uint SourceGfxObjId = 0u)
|
||||
{
|
||||
public bool ShouldRender => Reason is DirectionalShadowGateReason.Enabled;
|
||||
}
|
||||
|
||||
internal static class DirectionalShadowEnvironmentGate
|
||||
{
|
||||
private const float MinimumDirectionalEnergy = 1e-5f;
|
||||
|
||||
public static DirectionalShadowEnvironmentState Evaluate(
|
||||
in DirectionalShadowEnvironmentInput input,
|
||||
in DirectionalShadowAtmospherePolicy policy)
|
||||
{
|
||||
if (!input.PackEnabled)
|
||||
return Disabled(DirectionalShadowGateReason.PackDisabled);
|
||||
if (input.PortalOrLoginCoverVisible)
|
||||
return Disabled(DirectionalShadowGateReason.PortalOrLoginCover);
|
||||
if (input.PlayerInsideCell)
|
||||
return Disabled(DirectionalShadowGateReason.Indoor);
|
||||
|
||||
if (!input.Source.IsAvailable)
|
||||
return Disabled(DirectionalShadowGateReason.NoVisibleCelestial);
|
||||
|
||||
System.Numerics.Vector3 surfaceToLight =
|
||||
input.Source.SurfaceToLightDirection;
|
||||
float elevation = input.Source.ElevationSin;
|
||||
if (!float.IsFinite(elevation)
|
||||
|| elevation <= policy.MinimumLightElevationSin)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.SelectedLightBelowHorizon,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
1f,
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
float energy = input.Source.AuthoredEnergy;
|
||||
if (!float.IsFinite(energy)
|
||||
|| energy <= MinimumDirectionalEnergy)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.SelectedLightHasNoEnergy,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
1f,
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
float elevationSpan = MathF.Max(
|
||||
1e-5f,
|
||||
policy.FullStrengthLightElevationSin - policy.MinimumLightElevationSin);
|
||||
float elevationStrength = Math.Clamp(
|
||||
(elevation - policy.MinimumLightElevationSin) / elevationSpan,
|
||||
0f,
|
||||
1f);
|
||||
float weatherStrength = policy.StrengthFor(input.Atmosphere.Kind);
|
||||
float atmosphereProgress = Math.Clamp(input.Atmosphere.Intensity, 0f, 1f);
|
||||
float dayGroupStrength = Math.Clamp(input.ActiveDayGroupMultiplier, 0f, 1f);
|
||||
float strength = elevationStrength
|
||||
* Math.Clamp(energy, 0f, 1f)
|
||||
* weatherStrength
|
||||
* atmosphereProgress
|
||||
* dayGroupStrength;
|
||||
if (!float.IsFinite(strength) || strength <= 0f)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.AtmosphereSuppressed,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
policy.SoftnessFor(input.Atmosphere.Kind),
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.Enabled,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
Math.Clamp(strength, 0f, 1f),
|
||||
MathF.Max(1f, policy.SoftnessFor(input.Atmosphere.Kind)),
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
private static DirectionalShadowEnvironmentState Disabled(
|
||||
DirectionalShadowGateReason reason) =>
|
||||
new(reason, System.Numerics.Vector3.UnitZ, 0f, 0f, 1f);
|
||||
}
|
||||
154
src/AcDream.App/Rendering/DirectionalShadowReceiver.cs
Normal file
154
src/AcDream.App/Rendering/DirectionalShadowReceiver.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// The ordinary, non-ref view of the directional-shadow allocation produced for
|
||||
/// one frame. The serial prevents a ring slice from leaking into a later frame.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowFrameBinding(
|
||||
long FrameSerial,
|
||||
bool Enabled,
|
||||
IGpuBuffer? Buffer,
|
||||
uint OffsetBytes,
|
||||
uint SizeBytes,
|
||||
GpuTextureSlot TextureSlot,
|
||||
int CascadeCount)
|
||||
{
|
||||
internal static DirectionalShadowFrameBinding Disabled => default;
|
||||
|
||||
internal bool IsValidFor(IGpuFrame frame) =>
|
||||
Enabled
|
||||
&& Buffer is not null
|
||||
&& FrameSerial == frame.Serial
|
||||
&& SizeBytes == DirectionalShadowUniforms.SizeInBytes
|
||||
&& TextureSlot.IsAssigned
|
||||
&& CascadeCount is >= 2 and <= 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receiver-side seam. A pack runtime may publish this source at a stable frame
|
||||
/// boundary without exposing the producer's target or ref-struct allocation.
|
||||
/// </summary>
|
||||
internal interface IDirectionalShadowReceiverSource
|
||||
{
|
||||
DirectionalShadowPipelineShaders PipelineShaders { get; }
|
||||
|
||||
bool TryGetCurrentFrameBinding(
|
||||
IGpuFrame frame,
|
||||
out DirectionalShadowFrameBinding binding);
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowPipelineShaders(
|
||||
GpuShaderSet TerrainCaster,
|
||||
GpuShaderSet WorldOpaqueCaster,
|
||||
GpuShaderSet WorldAlphaCutoutCaster,
|
||||
GpuShaderSet TerrainReceiver,
|
||||
GpuShaderSet WorldReceiver)
|
||||
{
|
||||
internal DirectionalShadowMultiviewPipelineShaders? MultiviewCasters { get; init; }
|
||||
|
||||
internal static DirectionalShadowPipelineShaders Local { get; } = new(
|
||||
new GpuShaderSet("directional_shadow_terrain"),
|
||||
new GpuShaderSet("directional_shadow_world_opaque"),
|
||||
new GpuShaderSet("directional_shadow_world_cutout"),
|
||||
new GpuShaderSet("terrain_atmospheric"),
|
||||
new GpuShaderSet("mesh_atmospheric"))
|
||||
{
|
||||
MultiviewCasters = new DirectionalShadowMultiviewPipelineShaders(
|
||||
new GpuShaderSet("directional_shadow_terrain_multiview"),
|
||||
new GpuShaderSet("directional_shadow_world_opaque_multiview"),
|
||||
new GpuShaderSet("directional_shadow_world_cutout_multiview")),
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowMultiviewPipelineShaders(
|
||||
GpuShaderSet TerrainCaster,
|
||||
GpuShaderSet WorldOpaqueCaster,
|
||||
GpuShaderSet WorldAlphaCutoutCaster);
|
||||
|
||||
internal readonly record struct DirectionalShadowCascadeBlend(
|
||||
int PrimaryCascade,
|
||||
int SecondaryCascade,
|
||||
float SecondaryWeight,
|
||||
bool WithinShadowReach);
|
||||
|
||||
/// <summary>CPU mirror of receiver-only cascade and world-metre bias policy.</summary>
|
||||
internal static class DirectionalShadowReceiverPolicy
|
||||
{
|
||||
internal const string AtmosphericWorldPassName = "atmospheric-world-hdr";
|
||||
|
||||
internal static bool ShouldSelectReceiverPipeline(
|
||||
string passName,
|
||||
bool sourcePresent,
|
||||
bool bindingValid) =>
|
||||
sourcePresent
|
||||
&& bindingValid
|
||||
&& string.Equals(
|
||||
passName,
|
||||
AtmosphericWorldPassName,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
internal static DirectionalShadowCascadeBlend SelectCascade(
|
||||
float cameraDistanceMeters,
|
||||
Vector4 splitFarMeters,
|
||||
int cascadeCount,
|
||||
float blendWidthMeters)
|
||||
{
|
||||
if (!float.IsFinite(cameraDistanceMeters) || cameraDistanceMeters < 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(cameraDistanceMeters));
|
||||
if (cascadeCount is < 2 or > 4)
|
||||
throw new ArgumentOutOfRangeException(nameof(cascadeCount));
|
||||
if (!float.IsFinite(blendWidthMeters) || blendWidthMeters < 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(blendWidthMeters));
|
||||
|
||||
Span<float> splits = stackalloc float[4]
|
||||
{
|
||||
splitFarMeters.X,
|
||||
splitFarMeters.Y,
|
||||
splitFarMeters.Z,
|
||||
splitFarMeters.W,
|
||||
};
|
||||
for (int i = 0; i < cascadeCount; i++)
|
||||
{
|
||||
if (!float.IsFinite(splits[i])
|
||||
|| splits[i] <= 0f
|
||||
|| (i > 0 && splits[i] < splits[i - 1]))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Directional-shadow split distances must be finite, positive, and monotonic.",
|
||||
nameof(splitFarMeters));
|
||||
}
|
||||
}
|
||||
|
||||
int primary = 0;
|
||||
while (primary < cascadeCount && cameraDistanceMeters > splits[primary])
|
||||
primary++;
|
||||
if (primary == cascadeCount)
|
||||
return new DirectionalShadowCascadeBlend(cascadeCount - 1, cascadeCount - 1, 0f, false);
|
||||
|
||||
if (primary == cascadeCount - 1 || blendWidthMeters <= 0f)
|
||||
return new DirectionalShadowCascadeBlend(primary, primary, 0f, true);
|
||||
|
||||
float blendStart = MathF.Max(0f, splits[primary] - blendWidthMeters);
|
||||
float t = Math.Clamp(
|
||||
(cameraDistanceMeters - blendStart) / MathF.Max(blendWidthMeters, 1e-6f),
|
||||
0f,
|
||||
1f);
|
||||
float smooth = t * t * (3f - 2f * t);
|
||||
return new DirectionalShadowCascadeBlend(primary, primary + 1, smooth, true);
|
||||
}
|
||||
|
||||
internal static float ReceiverBiasMeters(
|
||||
in DirectionalShadowWorldBias bias,
|
||||
float normalDotSurfaceToLight) =>
|
||||
bias.ConstantDepthMeters
|
||||
+ bias.SlopeDepthMeters * (1f - Math.Clamp(normalDotSurfaceToLight, 0f, 1f));
|
||||
|
||||
internal static bool ShouldSample(
|
||||
bool bindingEnabled,
|
||||
bool indoor,
|
||||
bool hasSelectedCelestialDirectionalLight) =>
|
||||
bindingEnabled && !indoor && hasSelectedCelestialDirectionalLight;
|
||||
}
|
||||
439
src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs
Normal file
439
src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalShadowTransformPublishStats(
|
||||
bool TopologyUploaded,
|
||||
int DynamicMatricesUpdated,
|
||||
int DynamicRangesUpdated,
|
||||
long BytesWritten,
|
||||
int CurrentChangedMatrices = 0,
|
||||
int PendingReplayMatrices = 0,
|
||||
bool UsedFullDynamicFallback = false,
|
||||
bool DenseDirectUpload = false,
|
||||
bool DenseFlightReplay = false);
|
||||
|
||||
/// <summary>
|
||||
/// Pack-owned transform storage indexed by the RHI frame-flight slot. A slot is
|
||||
/// handed back only after its prior GPU submission retires, so stable topology
|
||||
/// can keep every static matrix in mapped storage and update only the exact
|
||||
/// dynamic indices already refreshed by <see cref="DirectionalShadowPreparedDraws"/>.
|
||||
/// No animation, scene lookup, or pose derivation happens here.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalShadowTransformBufferSet : IDisposable
|
||||
{
|
||||
private readonly IGpuDevice _device;
|
||||
private SlotState[] _slots = [];
|
||||
private ulong _denseTopologyBuildSequence;
|
||||
private ulong _denseRevision = 1;
|
||||
private bool _disposed;
|
||||
|
||||
internal DirectionalShadowTransformBufferSet(IGpuDevice device)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
if (!device.Capabilities.SupportsPersistentlyMappedRings)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Directional-shadow retained transforms require persistently mapped host-writable buffers.");
|
||||
}
|
||||
}
|
||||
|
||||
internal long RetainedGpuBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
total = checked(total + (_slots[i].Buffer?.SizeBytes ?? 0L));
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
internal int BufferCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
if (_slots[i].Buffer is not null)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
internal long RetainedScratchBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long bytes = checked((long)_slots.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<SlotState>());
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
bytes = checked(bytes + (_slots[index].Pending?.RetainedBytes ?? 0L));
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
internal DirectionalShadowTransformPublishStats LastStats { get; private set; }
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots)
|
||||
{
|
||||
return Publish(
|
||||
frame,
|
||||
topologyBuildSequence,
|
||||
transforms,
|
||||
dynamicTransformSlots,
|
||||
dynamicTransformSlots,
|
||||
denseRefresh: false);
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots,
|
||||
ReadOnlySpan<int> allDynamicTransformSlots)
|
||||
{
|
||||
return Publish(
|
||||
frame,
|
||||
topologyBuildSequence,
|
||||
transforms,
|
||||
dynamicTransformSlots,
|
||||
allDynamicTransformSlots,
|
||||
denseRefresh: false);
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots,
|
||||
ReadOnlySpan<int> allDynamicTransformSlots,
|
||||
bool denseRefresh,
|
||||
uint bindingSizeBytes = 0)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
if (topologyBuildSequence == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(topologyBuildSequence));
|
||||
uint requiredInstances = checked((uint)transforms.Length);
|
||||
if (bindingSizeBytes == 0)
|
||||
{
|
||||
bindingSizeBytes = WorldTransformCapacityPolicy.ResolveBindingSizeBytes(
|
||||
requiredInstances,
|
||||
_device.Capabilities.MaxStorageBufferRangeBytes);
|
||||
}
|
||||
WorldTransformCapacityPolicy.ValidateBindingSizeBytes(
|
||||
bindingSizeBytes,
|
||||
requiredInstances,
|
||||
_device.Capabilities.MaxStorageBufferRangeBytes);
|
||||
if (!denseRefresh)
|
||||
ValidateDynamicSlots(dynamicTransformSlots, transforms.Length);
|
||||
ResetDenseRevisionForTopology(topologyBuildSequence);
|
||||
if (denseRefresh)
|
||||
{
|
||||
ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length);
|
||||
if (_denseRevision == ulong.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow dense transform revision was exhausted.");
|
||||
}
|
||||
_denseRevision++;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
_slots[index].Pending?.Clear();
|
||||
}
|
||||
EnsureSlotCapacity(frame.SlotIndex);
|
||||
ref SlotState slot = ref _slots[frame.SlotIndex];
|
||||
bool matchingSlot = slot.Buffer is not null
|
||||
&& slot.TopologyBuildSequence == topologyBuildSequence
|
||||
&& slot.TransformCount == transforms.Length
|
||||
&& slot.Buffer.SizeBytes >= bindingSizeBytes;
|
||||
bool denseFlightReplay = matchingSlot
|
||||
&& slot.ConsumedDenseRevision != _denseRevision;
|
||||
int pendingReplayMatrices = matchingSlot
|
||||
? slot.Pending?.Count ?? 0
|
||||
: 0;
|
||||
if (!denseRefresh)
|
||||
{
|
||||
MarkPendingChanges(
|
||||
topologyBuildSequence,
|
||||
transforms.Length,
|
||||
dynamicTransformSlots);
|
||||
}
|
||||
int contentBytes = checked(transforms.Length * 64);
|
||||
int allocationBytes = checked((int)bindingSizeBytes);
|
||||
|
||||
if (slot.Buffer is null
|
||||
|| slot.TopologyBuildSequence != topologyBuildSequence
|
||||
|| slot.TransformCount != transforms.Length
|
||||
|| slot.Buffer.SizeBytes < allocationBytes)
|
||||
{
|
||||
IGpuBuffer? candidate = null;
|
||||
try
|
||||
{
|
||||
candidate = _device.CreateBuffer(new GpuBufferDescription(
|
||||
$"directional-shadow-transforms-slot-{frame.SlotIndex}-build-{topologyBuildSequence}",
|
||||
allocationBytes,
|
||||
GpuBufferUsage.Storage | GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.HostWritable));
|
||||
if (!candidate.HostWritesAreCoherent)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Directional-shadow retained transforms require coherent "
|
||||
+ "host-writable Vulkan memory. The pack will fail safe "
|
||||
+ "on this adapter rather than expose unflushed pose data.");
|
||||
}
|
||||
if (!transforms.IsEmpty)
|
||||
{
|
||||
candidate.Upload(0, MemoryMarshal.AsBytes(transforms));
|
||||
frame.PublishHostStorageWrites(candidate);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
candidate?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
IGpuBuffer? previous = slot.Buffer;
|
||||
PendingTransformSet pending = slot.Pending
|
||||
?? new PendingTransformSet(transforms.Length);
|
||||
pending.EnsureCapacity(transforms.Length);
|
||||
pending.Clear();
|
||||
slot = new SlotState(
|
||||
candidate,
|
||||
topologyBuildSequence,
|
||||
transforms.Length,
|
||||
pending,
|
||||
_denseRevision);
|
||||
previous?.Dispose();
|
||||
LastStats = new DirectionalShadowTransformPublishStats(
|
||||
TopologyUploaded: true,
|
||||
DynamicMatricesUpdated: 0,
|
||||
DynamicRangesUpdated: 0,
|
||||
BytesWritten: contentBytes,
|
||||
CurrentChangedMatrices: dynamicTransformSlots.Length,
|
||||
PendingReplayMatrices: 0,
|
||||
DenseDirectUpload: denseRefresh);
|
||||
}
|
||||
else
|
||||
{
|
||||
PendingTransformSet pending = slot.Pending
|
||||
?? throw new InvalidOperationException(
|
||||
"A retained directional-shadow flight slot has no pending-change owner.");
|
||||
bool directDenseUpload = denseRefresh || denseFlightReplay;
|
||||
ReadOnlySpan<int> slotsToUpload = directDenseUpload
|
||||
? allDynamicTransformSlots
|
||||
: pending.GetSorted();
|
||||
if (directDenseUpload && !denseRefresh)
|
||||
ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length);
|
||||
int ranges = UploadDynamicRanges(
|
||||
slot.Buffer,
|
||||
transforms,
|
||||
slotsToUpload,
|
||||
out long bytesWritten);
|
||||
if (ranges != 0)
|
||||
frame.PublishHostStorageWrites(slot.Buffer);
|
||||
LastStats = new DirectionalShadowTransformPublishStats(
|
||||
TopologyUploaded: false,
|
||||
DynamicMatricesUpdated: slotsToUpload.Length,
|
||||
DynamicRangesUpdated: ranges,
|
||||
BytesWritten: bytesWritten,
|
||||
CurrentChangedMatrices: dynamicTransformSlots.Length,
|
||||
PendingReplayMatrices: directDenseUpload ? 0 : pendingReplayMatrices,
|
||||
DenseDirectUpload: denseRefresh,
|
||||
DenseFlightReplay: denseFlightReplay && !denseRefresh);
|
||||
pending.Clear();
|
||||
slot = slot with { ConsumedDenseRevision = _denseRevision };
|
||||
}
|
||||
|
||||
IGpuBuffer buffer = slot.Buffer
|
||||
?? throw new InvalidOperationException(
|
||||
"The retained directional-shadow transform buffer was not published.");
|
||||
return new WorldTransformFrameSlice(
|
||||
frame.Serial,
|
||||
buffer,
|
||||
BaseOffsetBytes: 0,
|
||||
checked((uint)buffer.SizeBytes),
|
||||
FirstInstance: 0,
|
||||
checked((uint)transforms.Length));
|
||||
}
|
||||
|
||||
private void ResetDenseRevisionForTopology(ulong topologyBuildSequence)
|
||||
{
|
||||
if (_denseTopologyBuildSequence == topologyBuildSequence)
|
||||
return;
|
||||
_denseTopologyBuildSequence = topologyBuildSequence;
|
||||
_denseRevision = 1;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
_slots[index].Pending?.Clear();
|
||||
}
|
||||
|
||||
private void MarkPendingChanges(
|
||||
ulong topologyBuildSequence,
|
||||
int transformCount,
|
||||
ReadOnlySpan<int> dynamicTransformSlots)
|
||||
{
|
||||
if (dynamicTransformSlots.IsEmpty)
|
||||
return;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
{
|
||||
ref SlotState candidate = ref _slots[index];
|
||||
if (candidate.Buffer is null
|
||||
|| candidate.TopologyBuildSequence != topologyBuildSequence
|
||||
|| candidate.TransformCount != transformCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PendingTransformSet pending = candidate.Pending
|
||||
??= new PendingTransformSet(transformCount);
|
||||
pending.EnsureCapacity(transformCount);
|
||||
pending.Mark(dynamicTransformSlots);
|
||||
}
|
||||
}
|
||||
|
||||
private static int UploadDynamicRanges(
|
||||
IGpuBuffer buffer,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> slots,
|
||||
out long bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
int ranges = 0;
|
||||
int cursor = 0;
|
||||
while (cursor < slots.Length)
|
||||
{
|
||||
int start = slots[cursor];
|
||||
int end = start + 1;
|
||||
cursor++;
|
||||
while (cursor < slots.Length && slots[cursor] == end)
|
||||
{
|
||||
end++;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
ReadOnlySpan<Matrix4x4> values = transforms.Slice(start, end - start);
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(values);
|
||||
buffer.Upload(checked((long)start * 64L), bytes);
|
||||
bytesWritten = checked(bytesWritten + bytes.Length);
|
||||
ranges++;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static void ValidateDynamicSlots(
|
||||
ReadOnlySpan<int> slots,
|
||||
int transformCount)
|
||||
{
|
||||
int previous = -1;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
int current = slots[i];
|
||||
if ((uint)current >= (uint)transformCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Dynamic shadow transform slot {current} is outside the "
|
||||
+ $"{transformCount}-matrix retained product.");
|
||||
}
|
||||
if (current <= previous)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow transform slots must be strictly increasing.");
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSlotCapacity(int slotIndex)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(slotIndex);
|
||||
if (_slots.Length > slotIndex)
|
||||
return;
|
||||
int capacity = _slots.Length == 0 ? 2 : _slots.Length;
|
||||
while (capacity <= slotIndex)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref _slots, capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
_slots[i].Buffer?.Dispose();
|
||||
_slots[i] = default;
|
||||
}
|
||||
LastStats = default;
|
||||
_denseTopologyBuildSequence = 0;
|
||||
_denseRevision = 0;
|
||||
}
|
||||
|
||||
private record struct SlotState(
|
||||
IGpuBuffer? Buffer,
|
||||
ulong TopologyBuildSequence,
|
||||
int TransformCount,
|
||||
PendingTransformSet? Pending,
|
||||
ulong ConsumedDenseRevision);
|
||||
|
||||
private sealed class PendingTransformSet
|
||||
{
|
||||
private int[] _slots;
|
||||
private bool[] _marked;
|
||||
|
||||
internal PendingTransformSet(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
_slots = new int[capacity];
|
||||
_marked = new bool[capacity];
|
||||
}
|
||||
|
||||
internal int Count { get; private set; }
|
||||
|
||||
internal long RetainedBytes => checked(
|
||||
(long)_slots.Length * sizeof(int) + _marked.Length);
|
||||
|
||||
internal void EnsureCapacity(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
if (_slots.Length >= capacity)
|
||||
return;
|
||||
Array.Resize(ref _slots, capacity);
|
||||
Array.Resize(ref _marked, capacity);
|
||||
}
|
||||
|
||||
internal void Mark(ReadOnlySpan<int> slots)
|
||||
{
|
||||
for (int index = 0; index < slots.Length; index++)
|
||||
{
|
||||
int slot = slots[index];
|
||||
if (_marked[slot])
|
||||
continue;
|
||||
_marked[slot] = true;
|
||||
_slots[Count++] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
internal ReadOnlySpan<int> GetSorted()
|
||||
{
|
||||
Array.Sort(_slots, 0, Count);
|
||||
return _slots.AsSpan(0, Count);
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
for (int index = 0; index < Count; index++)
|
||||
_marked[_slots[index]] = false;
|
||||
Count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/AcDream.App/Rendering/DirectionalShadowUniforms.cs
Normal file
111
src/AcDream.App/Rendering/DirectionalShadowUniforms.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Shader ABI SSOT companion for opt-in set 3 binding 6. The matching GLSL block is
|
||||
/// directional_shadow_common.glsl; both are pinned at 336 std140 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct DirectionalShadowUniforms
|
||||
{
|
||||
internal const int SizeInBytes = 336;
|
||||
|
||||
public readonly Matrix4x4 WorldToClip0;
|
||||
public readonly Matrix4x4 WorldToClip1;
|
||||
public readonly Matrix4x4 WorldToClip2;
|
||||
public readonly Matrix4x4 WorldToClip3;
|
||||
public readonly Vector4 SplitFarMeters;
|
||||
public readonly Vector4 Control;
|
||||
public readonly Vector4 BiasMeters;
|
||||
public readonly UInt4 TextureAndFlags;
|
||||
public readonly Vector4 LightDirectionAndSource;
|
||||
|
||||
internal DirectionalShadowUniforms(
|
||||
Matrix4x4 worldToClip0,
|
||||
Matrix4x4 worldToClip1,
|
||||
Matrix4x4 worldToClip2,
|
||||
Matrix4x4 worldToClip3,
|
||||
Vector4 splitFarMeters,
|
||||
Vector4 control,
|
||||
Vector4 biasMeters,
|
||||
UInt4 textureAndFlags,
|
||||
Vector4 lightDirectionAndSource)
|
||||
{
|
||||
WorldToClip0 = worldToClip0;
|
||||
WorldToClip1 = worldToClip1;
|
||||
WorldToClip2 = worldToClip2;
|
||||
WorldToClip3 = worldToClip3;
|
||||
SplitFarMeters = splitFarMeters;
|
||||
Control = control;
|
||||
BiasMeters = biasMeters;
|
||||
TextureAndFlags = textureAndFlags;
|
||||
LightDirectionAndSource = lightDirectionAndSource;
|
||||
}
|
||||
|
||||
internal static DirectionalShadowUniforms Create(
|
||||
ReadOnlySpan<DirectionalShadowCascade> cascades,
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
in DirectionalShadowQuality quality,
|
||||
GpuTextureSlot textureSlot)
|
||||
{
|
||||
if (cascades.Length != quality.CascadeCount)
|
||||
throw new ArgumentException("The cascade span must match the selected quality.", nameof(cascades));
|
||||
if (!textureSlot.IsAssigned)
|
||||
throw new ArgumentException("The directional depth array requires an assigned texture slot.", nameof(textureSlot));
|
||||
|
||||
Matrix4x4 matrix0 = cascades[0].WorldToShadowClip;
|
||||
Matrix4x4 matrix1 = cascades.Length > 1 ? cascades[1].WorldToShadowClip : Matrix4x4.Identity;
|
||||
Matrix4x4 matrix2 = cascades.Length > 2 ? cascades[2].WorldToShadowClip : Matrix4x4.Identity;
|
||||
Matrix4x4 matrix3 = cascades.Length > 3 ? cascades[3].WorldToShadowClip : Matrix4x4.Identity;
|
||||
float split0 = cascades[0].SplitFarMeters;
|
||||
float split1 = cascades.Length > 1 ? cascades[1].SplitFarMeters : quality.MaximumReachMeters;
|
||||
float split2 = cascades.Length > 2 ? cascades[2].SplitFarMeters : quality.MaximumReachMeters;
|
||||
float split3 = cascades.Length > 3 ? cascades[3].SplitFarMeters : quality.MaximumReachMeters;
|
||||
|
||||
// The pinned v1 receiver block carries one world-space bias triple.
|
||||
// Publish the conservative outer-cascade values; the receiver derives
|
||||
// each inner cascade's relative texel footprint from its projection
|
||||
// matrix before applying this triple. That preserves the bias portion
|
||||
// of the v1 ABI while the selected-light vec4 is appended at byte 320
|
||||
// without applying the outer map's visibly excessive offset nearby.
|
||||
DirectionalShadowWorldBias bias = cascades[^1].Bias;
|
||||
float effectiveReachMeters = cascades[^1].SplitFarMeters;
|
||||
return new DirectionalShadowUniforms(
|
||||
matrix0,
|
||||
matrix1,
|
||||
matrix2,
|
||||
matrix3,
|
||||
new Vector4(split0, split1, split2, split3),
|
||||
new Vector4(
|
||||
environment.Strength,
|
||||
environment.SoftnessMultiplier,
|
||||
effectiveReachMeters,
|
||||
MathF.Max(1f, effectiveReachMeters * 0.02f)),
|
||||
new Vector4(
|
||||
bias.ConstantDepthMeters,
|
||||
bias.SlopeDepthMeters,
|
||||
bias.NormalOffsetMeters,
|
||||
cascades[0].CasterDepthPaddingMeters),
|
||||
new UInt4(
|
||||
textureSlot.Index,
|
||||
checked((uint)quality.CascadeCount),
|
||||
checked((uint)quality.MapResolution),
|
||||
1u | (checked((uint)quality.PcfRadiusTexels) << 8)),
|
||||
new Vector4(
|
||||
environment.SurfaceToLightDirection,
|
||||
checked((uint)environment.SourceKind)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Four uints with the exact 16-byte std140 uvec4 representation.</summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct UInt4(uint x, uint y, uint z, uint w)
|
||||
{
|
||||
public readonly uint X = x;
|
||||
public readonly uint Y = y;
|
||||
public readonly uint Z = z;
|
||||
public readonly uint W = w;
|
||||
}
|
||||
951
src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs
Normal file
951
src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalSunShadowRenderInput(
|
||||
DirectionalShadowEnvironmentInput Environment,
|
||||
Matrix4x4 CameraView,
|
||||
Matrix4x4 CameraProjection,
|
||||
DirectionalShadowCasterFrame Casters,
|
||||
float CameraNearMeters = 0.1f,
|
||||
float CasterDepthPaddingMeters = 48f,
|
||||
float ResidentMaximumReachMeters = float.PositiveInfinity,
|
||||
bool MeasureGpuTimers = true,
|
||||
bool MeasureCpuStages = false);
|
||||
|
||||
internal readonly record struct DirectionalSunShadowCpuStageTicks(
|
||||
long EnvironmentGateTicks,
|
||||
long PreparedDrawsAndTransformsTicks,
|
||||
long FitAndUniformTicks,
|
||||
long LayeredPassRecordingTicks,
|
||||
long BookkeepingTicks);
|
||||
|
||||
internal readonly record struct DirectionalShadowTransformChurnDiagnostics(
|
||||
int CopiedSceneChanges,
|
||||
int UpdateTransformChanges,
|
||||
int UpdateAppearanceChanges,
|
||||
int DynamicSynchronizationChanges,
|
||||
int ActiveAnimatedStaticChanges,
|
||||
int LiveDynamicRootChanges,
|
||||
int EquippedChildChanges,
|
||||
int DedupedCasterSlots,
|
||||
bool SceneJournalFullRefresh,
|
||||
bool DensityBulkRefresh,
|
||||
int BatchedProjectionCopyCalls,
|
||||
int ChangedMatrixSlots,
|
||||
int FlightCurrentChangedMatrices,
|
||||
int FlightPendingReplayMatrices,
|
||||
int FlightUploadedMatrices,
|
||||
int FlightUploadRanges,
|
||||
long FlightBytesWritten,
|
||||
bool FlightFullDynamicFallback,
|
||||
bool DenseDirectUpload,
|
||||
bool DenseFlightReplay,
|
||||
DirectionalShadowCasterClassDiagnostics CasterClasses = default);
|
||||
|
||||
internal readonly record struct DirectionalSunShadowDiagnostics(
|
||||
DirectionalShadowGateReason GateReason,
|
||||
float Strength,
|
||||
int CascadeCount,
|
||||
int DrawCalls,
|
||||
int WorldOpaqueCommands,
|
||||
int WorldAlphaCutoutCommands,
|
||||
int TerrainCommands,
|
||||
ulong WorldPreparationSequence,
|
||||
ulong TerrainPreparationSequence,
|
||||
double CpuMilliseconds,
|
||||
double LastResolvedGpuMilliseconds,
|
||||
bool HasResolvedGpuMeasurement,
|
||||
long ResidentDepthBytes,
|
||||
DirectionalSunShadowCpuStageTicks CpuStages = default,
|
||||
DirectionalShadowTransformChurnDiagnostics TransformChurn = default,
|
||||
AuthoredCelestialShadowSourceKind SourceKind =
|
||||
AuthoredCelestialShadowSourceKind.None,
|
||||
int SourceObjectIndex = -1,
|
||||
uint SourceGfxObjId = 0u,
|
||||
Vector3 SurfaceToLightDirection = default,
|
||||
float LightElevationSin = 0f);
|
||||
|
||||
internal static class DirectionalShadowBatchFlags
|
||||
{
|
||||
internal const uint AlphaCutout = 1u << 0;
|
||||
internal static uint Encode(DirectionalShadowCasterMaterial material) =>
|
||||
material is DirectionalShadowCasterMaterial.AlphaCutout
|
||||
? AlphaCutout
|
||||
: 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tier-2 producer only: fits selected celestial-light cascades and records
|
||||
/// their depth maps.
|
||||
/// It does not alter the retail world pass or sample shadows in receivers.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverSource, IDisposable
|
||||
{
|
||||
internal const string TimerPrefix = "directional-shadow-cascade-";
|
||||
internal const string MultiviewTimerName = "directional-shadow-multiview";
|
||||
internal const uint LowMultiviewMask = 0b11;
|
||||
private const int DrawCommandStride = 20;
|
||||
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly DirectionalShadowQuality _quality;
|
||||
private readonly DirectionalShadowAtmospherePolicy _atmospherePolicy;
|
||||
private readonly DirectionalShadowPipelineShaders _pipelineShaders;
|
||||
private readonly bool _multiviewCascades;
|
||||
private readonly IGpuDirectionalDepthTarget _target;
|
||||
private readonly IGpuSampler _sampler;
|
||||
private readonly GpuTextureSlot _textureSlot;
|
||||
private readonly IGpuPipeline _terrainPipeline;
|
||||
private readonly IGpuPipeline _worldOpaquePipeline;
|
||||
private readonly IGpuPipeline _worldCutoutPipeline;
|
||||
private readonly IGpuPipeline? _terrainMultiviewPipeline;
|
||||
private readonly IGpuPipeline? _worldOpaqueMultiviewPipeline;
|
||||
private readonly IGpuPipeline? _worldCutoutMultiviewPipeline;
|
||||
private readonly DirectionalShadowTransformBufferSet _transformBuffers;
|
||||
private readonly DirectionalShadowCascade[] _cascades = new DirectionalShadowCascade[4];
|
||||
private DirectionalShadowBatchGpuData[] _batchScratch = [];
|
||||
private IGpuBuffer? _worldBatchBuffer;
|
||||
private IGpuBuffer? _worldCommandBuffer;
|
||||
private IGpuBuffer? _terrainCommandBuffer;
|
||||
private ulong _worldGpuBuildSequence;
|
||||
private ulong _terrainGpuBuildSequence;
|
||||
private DirectionalShadowFrameBinding _currentFrameBinding;
|
||||
private bool _disposed;
|
||||
|
||||
internal DirectionalSunShadowRenderer(
|
||||
IGpuDevice device,
|
||||
DirectionalShadowPreset preset,
|
||||
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
|
||||
DirectionalShadowPipelineShaders? pipelineShaders = null,
|
||||
bool multiviewCascades = false)
|
||||
: this(
|
||||
device,
|
||||
DirectionalShadowQuality.For(preset),
|
||||
atmospherePolicy,
|
||||
pipelineShaders,
|
||||
multiviewCascades)
|
||||
{
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowRenderer(
|
||||
IGpuDevice device,
|
||||
DirectionalShadowQuality quality,
|
||||
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
|
||||
DirectionalShadowPipelineShaders? pipelineShaders = null,
|
||||
bool multiviewCascades = false)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
if (quality.CascadeCount is < 1 or > 4
|
||||
|| quality.MapResolution <= 0
|
||||
|| !float.IsFinite(quality.MaximumReachMeters)
|
||||
|| quality.MaximumReachMeters <= 0f
|
||||
|| quality.PcfRadiusTexels is < 0 or > 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(quality),
|
||||
"Directional-shadow quality must declare 1..4 cascades, a positive "
|
||||
+ "resolution/reach, and a 0..2 PCF radius.");
|
||||
}
|
||||
_quality = quality;
|
||||
_atmospherePolicy = atmospherePolicy ?? DirectionalShadowAtmospherePolicy.BuiltIn;
|
||||
_pipelineShaders = pipelineShaders ?? DirectionalShadowPipelineShaders.Local;
|
||||
_multiviewCascades = multiviewCascades;
|
||||
if (multiviewCascades && quality.CascadeCount != 2)
|
||||
throw new NotSupportedException("The multiview shadow hint requires exactly two Low cascades.");
|
||||
if (multiviewCascades && !device.Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected device does not support multiview shadow cascades.");
|
||||
if (multiviewCascades && _pipelineShaders.MultiviewCasters is null)
|
||||
throw new NotSupportedException("The pack did not declare all multiview shadow caster variants.");
|
||||
|
||||
IGpuDirectionalDepthTarget? target = null;
|
||||
IGpuSampler? sampler = null;
|
||||
GpuTextureSlot textureSlot = GpuTextureSlot.Unassigned;
|
||||
IGpuPipeline? terrain = null;
|
||||
IGpuPipeline? opaque = null;
|
||||
IGpuPipeline? cutout = null;
|
||||
IGpuPipeline? terrainMultiview = null;
|
||||
IGpuPipeline? opaqueMultiview = null;
|
||||
IGpuPipeline? cutoutMultiview = null;
|
||||
DirectionalShadowTransformBufferSet? transformBuffers = null;
|
||||
try
|
||||
{
|
||||
target = device.CreateDirectionalDepthTarget(
|
||||
new GpuDirectionalDepthTargetDescription(
|
||||
$"directional-shadow-{quality.Preset.ToString().ToLowerInvariant()}",
|
||||
_quality.MapResolution,
|
||||
_quality.CascadeCount));
|
||||
sampler = device.CreateSampler(GpuSamplerDescription.ShadowNearestClamp);
|
||||
textureSlot = device.RegisterTexture(target.DepthTexture, sampler);
|
||||
terrain = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-terrain",
|
||||
_pipelineShaders.TerrainCaster,
|
||||
TerrainModernRenderer.TerrainVertexLayout,
|
||||
GpuFrontFace.CounterClockwise);
|
||||
opaque = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-world-opaque",
|
||||
_pipelineShaders.WorldOpaqueCaster,
|
||||
GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise);
|
||||
cutout = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-world-cutout",
|
||||
_pipelineShaders.WorldAlphaCutoutCaster,
|
||||
GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise);
|
||||
if (multiviewCascades)
|
||||
{
|
||||
DirectionalShadowMultiviewPipelineShaders shaders =
|
||||
_pipelineShaders.MultiviewCasters!.Value;
|
||||
terrainMultiview = CreatePipeline(device, "directional-shadow-terrain-multiview",
|
||||
shaders.TerrainCaster, TerrainModernRenderer.TerrainVertexLayout,
|
||||
GpuFrontFace.CounterClockwise, LowMultiviewMask);
|
||||
opaqueMultiview = CreatePipeline(device, "directional-shadow-world-opaque-multiview",
|
||||
shaders.WorldOpaqueCaster, GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise, LowMultiviewMask);
|
||||
cutoutMultiview = CreatePipeline(device, "directional-shadow-world-cutout-multiview",
|
||||
shaders.WorldAlphaCutoutCaster, GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise, LowMultiviewMask);
|
||||
}
|
||||
transformBuffers = new DirectionalShadowTransformBufferSet(device);
|
||||
}
|
||||
catch
|
||||
{
|
||||
transformBuffers?.Dispose();
|
||||
cutoutMultiview?.Dispose();
|
||||
opaqueMultiview?.Dispose();
|
||||
terrainMultiview?.Dispose();
|
||||
cutout?.Dispose();
|
||||
opaque?.Dispose();
|
||||
terrain?.Dispose();
|
||||
if (textureSlot.IsAssigned)
|
||||
device.ReleaseTextureSlot(textureSlot);
|
||||
sampler?.Dispose();
|
||||
target?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
_target = target;
|
||||
_sampler = sampler;
|
||||
_textureSlot = textureSlot;
|
||||
_terrainPipeline = terrain;
|
||||
_worldOpaquePipeline = opaque;
|
||||
_worldCutoutPipeline = cutout;
|
||||
_terrainMultiviewPipeline = terrainMultiview;
|
||||
_worldOpaqueMultiviewPipeline = opaqueMultiview;
|
||||
_worldCutoutMultiviewPipeline = cutoutMultiview;
|
||||
_transformBuffers = transformBuffers;
|
||||
}
|
||||
|
||||
internal DirectionalShadowQuality Quality => _quality;
|
||||
|
||||
internal bool MultiviewCascadesEnabled => _multiviewCascades;
|
||||
|
||||
internal static string TimerName(int cascadeIndex) => cascadeIndex switch
|
||||
{
|
||||
0 => "directional-shadow-cascade-0",
|
||||
1 => "directional-shadow-cascade-1",
|
||||
2 => "directional-shadow-cascade-2",
|
||||
3 => "directional-shadow-cascade-3",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(cascadeIndex)),
|
||||
};
|
||||
|
||||
internal IGpuTexture DepthTexture => _target.DepthTexture;
|
||||
|
||||
internal GpuTextureSlot TextureSlot => _textureSlot;
|
||||
|
||||
public DirectionalShadowPipelineShaders PipelineShaders => _pipelineShaders;
|
||||
|
||||
internal DirectionalShadowFrameBinding CurrentFrameBinding => _currentFrameBinding;
|
||||
|
||||
/// <summary>
|
||||
/// Topology-only command metadata lives in pack-owned device-local buffers.
|
||||
/// It is rebuilt transactionally when the retained CPU product changes and
|
||||
/// is never copied through a per-frame ring on a stable scene.
|
||||
/// </summary>
|
||||
internal long RetainedCommandBufferBytes => checked(
|
||||
(_worldBatchBuffer?.SizeBytes ?? 0L)
|
||||
+ (_worldCommandBuffer?.SizeBytes ?? 0L)
|
||||
+ (_terrainCommandBuffer?.SizeBytes ?? 0L));
|
||||
|
||||
internal int RetainedCommandBufferCount =>
|
||||
(_worldBatchBuffer is null ? 0 : 1)
|
||||
+ (_worldCommandBuffer is null ? 0 : 1)
|
||||
+ (_terrainCommandBuffer is null ? 0 : 1);
|
||||
|
||||
internal long RetainedGpuBufferBytes => checked(
|
||||
RetainedCommandBufferBytes + _transformBuffers.RetainedGpuBytes);
|
||||
|
||||
internal int RetainedGpuBufferCount => checked(
|
||||
RetainedCommandBufferCount + _transformBuffers.BufferCount);
|
||||
|
||||
public bool TryGetCurrentFrameBinding(
|
||||
IGpuFrame frame,
|
||||
out DirectionalShadowFrameBinding binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
binding = _currentFrameBinding;
|
||||
return !_disposed && binding.IsValidFor(frame);
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowDiagnostics Render(
|
||||
IGpuFrame frame,
|
||||
in DirectionalSunShadowRenderInput input,
|
||||
WbDrawDispatcher world,
|
||||
TerrainModernRenderer terrain)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
ArgumentNullException.ThrowIfNull(terrain);
|
||||
long cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
DirectionalShadowEnvironmentState environment =
|
||||
DirectionalShadowEnvironmentGate.Evaluate(
|
||||
input.Environment,
|
||||
_atmospherePolicy);
|
||||
long environmentGateTicks = input.MeasureCpuStages
|
||||
? Stopwatch.GetTimestamp() - cpuStageStarted
|
||||
: 0L;
|
||||
if (!environment.ShouldRender)
|
||||
return Disabled(
|
||||
in environment,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks, 0L, 0L, 0L, 0L));
|
||||
if (input.ResidentMaximumReachMeters <= input.CameraNearMeters)
|
||||
{
|
||||
environment = environment with
|
||||
{
|
||||
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
|
||||
};
|
||||
return Disabled(
|
||||
in environment,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks, 0L, 0L, 0L, 0L));
|
||||
}
|
||||
|
||||
cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
DirectionalShadowPreparedDraws worldDraws =
|
||||
world.PrepareDirectionalShadowDraws(input.Casters);
|
||||
DirectionalShadowTerrainPreparedDraws terrainDraws =
|
||||
terrain.PrepareDirectionalShadowDraws();
|
||||
DirectionalShadowMeshGeometry? worldGeometry =
|
||||
worldDraws.Commands.IsEmpty ? null : world.GetDirectionalShadowGeometry();
|
||||
DirectionalShadowTerrainGeometry? terrainGeometry =
|
||||
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
|
||||
uint transformBindingSizeBytes =
|
||||
world.ResolveDirectionalShadowTransformBindingSize(
|
||||
worldDraws.Transforms.Length,
|
||||
// Stats counts every current WB source render batch before
|
||||
// transparent/cutout shadow rejection. Ordinary WB submission
|
||||
// publishes at most one matrix per source batch, making this a
|
||||
// complete-frame upper bound available before shadow commands
|
||||
// bind the one authoritative pose buffer.
|
||||
worldDraws.Stats.SourceBatches);
|
||||
WorldTransformFrameSlice retainedTransforms = _transformBuffers.Publish(
|
||||
frame,
|
||||
worldDraws.BuildSequence,
|
||||
worldDraws.Transforms,
|
||||
worldDraws.DynamicTransformSlots,
|
||||
worldDraws.AllDynamicTransformSlots,
|
||||
worldDraws.LastDynamicTransformRefreshWasDense,
|
||||
transformBindingSizeBytes);
|
||||
WorldTransformFrameSlice transforms =
|
||||
world.BeginDirectionalShadowTransformFrame(
|
||||
frame,
|
||||
in retainedTransforms);
|
||||
DirectionalShadowCasterBuildStats casterStats = input.Casters.Stats;
|
||||
DirectionalShadowCasterClassDiagnostics casterClasses =
|
||||
CompleteCasterClassDiagnostics(
|
||||
in casterStats,
|
||||
terrainDraws.Commands.Length);
|
||||
DirectionalShadowTransformPublishStats publishStats =
|
||||
_transformBuffers.LastStats;
|
||||
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
|
||||
casterStats.CopiedTransformChanges,
|
||||
casterStats.UpdateTransformChanges,
|
||||
casterStats.UpdateAppearanceChanges,
|
||||
casterStats.DynamicSynchronizationChanges,
|
||||
casterStats.ActiveAnimatedStaticChanges,
|
||||
casterStats.LiveDynamicRootChanges,
|
||||
casterStats.EquippedChildChanges,
|
||||
casterStats.DedupedChangedCasterSlots,
|
||||
casterStats.TransformJournalFullRefresh,
|
||||
casterStats.DensityBulkRefresh,
|
||||
casterStats.BatchedProjectionCopyCalls,
|
||||
worldDraws.LastDynamicTransformRefreshCount,
|
||||
publishStats.CurrentChangedMatrices,
|
||||
publishStats.PendingReplayMatrices,
|
||||
publishStats.DynamicMatricesUpdated,
|
||||
publishStats.DynamicRangesUpdated,
|
||||
publishStats.BytesWritten,
|
||||
publishStats.UsedFullDynamicFallback,
|
||||
publishStats.DenseDirectUpload,
|
||||
publishStats.DenseFlightReplay,
|
||||
casterClasses);
|
||||
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
|
||||
? Stopwatch.GetTimestamp() - cpuStageStarted
|
||||
: 0L;
|
||||
try
|
||||
{
|
||||
return RenderPrepared(
|
||||
frame,
|
||||
environment,
|
||||
input.CameraView,
|
||||
input.CameraProjection,
|
||||
input.CameraNearMeters,
|
||||
input.CasterDepthPaddingMeters,
|
||||
worldDraws,
|
||||
terrainDraws,
|
||||
worldGeometry,
|
||||
terrainGeometry,
|
||||
transforms,
|
||||
input.ResidentMaximumReachMeters,
|
||||
input.MeasureGpuTimers,
|
||||
input.MeasureCpuStages,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks,
|
||||
preparedDrawsAndTransformsTicks,
|
||||
0L,
|
||||
0L,
|
||||
0L),
|
||||
transformChurn);
|
||||
}
|
||||
catch
|
||||
{
|
||||
world.CancelDirectionalShadowTransformFrame(frame);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static DirectionalShadowCasterClassDiagnostics
|
||||
CompleteCasterClassDiagnostics(
|
||||
in DirectionalShadowCasterBuildStats casterStats,
|
||||
int terrainCommandCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(terrainCommandCount);
|
||||
return casterStats.CasterClasses with
|
||||
{
|
||||
TerrainCommands = terrainCommandCount,
|
||||
};
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowDiagnostics RenderPrepared(
|
||||
IGpuFrame frame,
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
Matrix4x4 cameraView,
|
||||
Matrix4x4 cameraProjection,
|
||||
float cameraNearMeters,
|
||||
float casterDepthPaddingMeters,
|
||||
DirectionalShadowPreparedDraws worldDraws,
|
||||
DirectionalShadowTerrainPreparedDraws terrainDraws,
|
||||
DirectionalShadowMeshGeometry? worldGeometry,
|
||||
DirectionalShadowTerrainGeometry? terrainGeometry,
|
||||
WorldTransformFrameSlice worldTransforms,
|
||||
float residentMaximumReachMeters = float.PositiveInfinity,
|
||||
bool measureGpuTimers = true,
|
||||
bool measureCpuStages = false,
|
||||
DirectionalSunShadowCpuStageTicks cpuStages = default,
|
||||
DirectionalShadowTransformChurnDiagnostics transformChurn = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
ArgumentNullException.ThrowIfNull(worldDraws);
|
||||
ArgumentNullException.ThrowIfNull(terrainDraws);
|
||||
if (!environment.ShouldRender)
|
||||
return Disabled(in environment, cpuStages);
|
||||
if (!worldDraws.Commands.IsEmpty && worldGeometry is null)
|
||||
throw new ArgumentNullException(nameof(worldGeometry));
|
||||
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
|
||||
throw new ArgumentNullException(nameof(terrainGeometry));
|
||||
if (!worldTransforms.IsValidFor(frame))
|
||||
throw new ArgumentException(
|
||||
"Shadow transforms must use this frame's shared N.5 allocation.",
|
||||
nameof(worldTransforms));
|
||||
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
var fit = new DirectionalShadowCascadeFitInput(
|
||||
cameraView,
|
||||
cameraProjection,
|
||||
environment.SurfaceToLightDirection,
|
||||
_quality,
|
||||
cameraNearMeters,
|
||||
PracticalSplitLambda: 0.65f,
|
||||
casterDepthPaddingMeters,
|
||||
residentMaximumReachMeters);
|
||||
int cascadeCount = DirectionalShadowCascadeFitter.Fit(
|
||||
fit,
|
||||
_cascades);
|
||||
if (cascadeCount == 0)
|
||||
{
|
||||
DirectionalShadowEnvironmentState unavailable = environment with
|
||||
{
|
||||
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
|
||||
};
|
||||
return Disabled(in unavailable, cpuStages);
|
||||
}
|
||||
|
||||
ReadOnlySpan<DirectionalShadowCascade> cascades =
|
||||
_cascades.AsSpan(0, cascadeCount);
|
||||
DirectionalShadowUniforms uniforms = DirectionalShadowUniforms.Create(
|
||||
cascades,
|
||||
environment,
|
||||
_quality,
|
||||
_textureSlot);
|
||||
GpuRingAllocation uniformAllocation = frame.AllocateRing(
|
||||
DirectionalShadowUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
MemoryMarshal.Write(uniformAllocation.Data, in uniforms);
|
||||
|
||||
long fitAndUniformFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
PreparedGpuUploads uploads = PrepareGpuData(
|
||||
worldTransforms,
|
||||
worldDraws,
|
||||
terrainDraws);
|
||||
if (MultiviewCascadesEnabled)
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(
|
||||
GpuPassDescription.DirectionalDepthMultiview(
|
||||
"directional-shadow-multiview",
|
||||
_target,
|
||||
LowMultiviewMask));
|
||||
using IDisposable? timer = measureGpuTimers
|
||||
? encoder.BeginTimerScope(MultiviewTimerName)
|
||||
: null;
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes);
|
||||
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, 0,
|
||||
_terrainMultiviewPipeline);
|
||||
DrawWorld(encoder, uploads, worldDraws, worldGeometry, 0,
|
||||
_worldOpaqueMultiviewPipeline, _worldCutoutMultiviewPipeline);
|
||||
}
|
||||
else for (int cascadeIndex = 0; cascadeIndex < cascadeCount; cascadeIndex++)
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(
|
||||
GpuPassDescription.DirectionalDepth(
|
||||
$"directional-shadow-{cascadeIndex}",
|
||||
_target,
|
||||
cascadeIndex));
|
||||
using IDisposable? timer = measureGpuTimers
|
||||
? encoder.BeginTimerScope(TimerName(cascadeIndex))
|
||||
: null;
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes);
|
||||
|
||||
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, cascadeIndex);
|
||||
DrawWorld(encoder, uploads, worldDraws, worldGeometry, cascadeIndex);
|
||||
}
|
||||
|
||||
long passRecordingFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
|
||||
_currentFrameBinding = new DirectionalShadowFrameBinding(
|
||||
frame.Serial,
|
||||
Enabled: true,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes,
|
||||
_textureSlot,
|
||||
cascadeCount);
|
||||
|
||||
(bool hasGpu, double gpuMilliseconds) = ResolveGpu(cascadeCount);
|
||||
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
|
||||
drawsPerCascade = checked(
|
||||
drawsPerCascade
|
||||
+ (worldDraws.Commands.IsEmpty
|
||||
? 0
|
||||
: worldDraws.OpaqueRuns.Length + worldDraws.AlphaCutoutRuns.Length));
|
||||
long finished = Stopwatch.GetTimestamp();
|
||||
cpuStages = cpuStages with
|
||||
{
|
||||
FitAndUniformTicks = measureCpuStages
|
||||
? fitAndUniformFinished - started
|
||||
: 0L,
|
||||
LayeredPassRecordingTicks = measureCpuStages
|
||||
? passRecordingFinished - fitAndUniformFinished
|
||||
: 0L,
|
||||
BookkeepingTicks = measureCpuStages
|
||||
? finished - passRecordingFinished
|
||||
: 0L,
|
||||
};
|
||||
return new DirectionalSunShadowDiagnostics(
|
||||
DirectionalShadowGateReason.Enabled,
|
||||
environment.Strength,
|
||||
cascadeCount,
|
||||
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
|
||||
worldDraws.OpaqueCommandCount,
|
||||
worldDraws.AlphaCutoutCommandCount,
|
||||
terrainDraws.Commands.Length,
|
||||
worldDraws.BuildSequence,
|
||||
terrainDraws.BuildSequence,
|
||||
(finished - started) * 1000d / Stopwatch.Frequency,
|
||||
gpuMilliseconds,
|
||||
hasGpu,
|
||||
_quality.ApproximateDepthMapBytes,
|
||||
cpuStages,
|
||||
transformChurn,
|
||||
environment.SourceKind,
|
||||
environment.SourceObjectIndex,
|
||||
environment.SourceGfxObjId,
|
||||
environment.SurfaceToLightDirection,
|
||||
environment.LightElevationSin);
|
||||
}
|
||||
|
||||
private PreparedGpuUploads PrepareGpuData(
|
||||
in WorldTransformFrameSlice transforms,
|
||||
DirectionalShadowPreparedDraws world,
|
||||
DirectionalShadowTerrainPreparedDraws terrain)
|
||||
{
|
||||
if (_worldGpuBuildSequence != world.BuildSequence)
|
||||
RebuildWorldGpuData(world);
|
||||
if (_terrainGpuBuildSequence != terrain.BuildSequence)
|
||||
RebuildTerrainGpuData(terrain);
|
||||
|
||||
return new PreparedGpuUploads(
|
||||
transforms,
|
||||
Slice(_worldBatchBuffer),
|
||||
Slice(_worldCommandBuffer),
|
||||
Slice(_terrainCommandBuffer));
|
||||
}
|
||||
|
||||
private void RebuildWorldGpuData(DirectionalShadowPreparedDraws world)
|
||||
{
|
||||
IGpuBuffer? batches = null;
|
||||
IGpuBuffer? commands = null;
|
||||
try
|
||||
{
|
||||
if (!world.Commands.IsEmpty)
|
||||
{
|
||||
EnsureBatchCapacity(world.Batches.Length);
|
||||
for (int i = 0; i < world.Batches.Length; i++)
|
||||
{
|
||||
DirectionalShadowPreparedBatch batch = world.Batches[i];
|
||||
_batchScratch[i] = new DirectionalShadowBatchGpuData(
|
||||
batch.TextureSlot.Index,
|
||||
0u,
|
||||
batch.TextureLayer,
|
||||
DirectionalShadowBatchFlags.Encode(batch.Material));
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
|
||||
_batchScratch.AsSpan(0, world.Batches.Length));
|
||||
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
|
||||
world.Commands);
|
||||
batches = CreateRetainedBuffer(
|
||||
$"directional-shadow-world-batches-{world.BuildSequence}",
|
||||
batchBytes,
|
||||
GpuBufferUsage.Storage);
|
||||
commands = CreateRetainedBuffer(
|
||||
$"directional-shadow-world-commands-{world.BuildSequence}",
|
||||
commandBytes,
|
||||
GpuBufferUsage.Indirect);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
commands?.Dispose();
|
||||
batches?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
IGpuBuffer? previousBatches = _worldBatchBuffer;
|
||||
IGpuBuffer? previousCommands = _worldCommandBuffer;
|
||||
_worldBatchBuffer = batches;
|
||||
_worldCommandBuffer = commands;
|
||||
_worldGpuBuildSequence = world.BuildSequence;
|
||||
previousCommands?.Dispose();
|
||||
previousBatches?.Dispose();
|
||||
}
|
||||
|
||||
private void RebuildTerrainGpuData(DirectionalShadowTerrainPreparedDraws terrain)
|
||||
{
|
||||
IGpuBuffer? commands = null;
|
||||
if (!terrain.Commands.IsEmpty)
|
||||
{
|
||||
commands = CreateRetainedBuffer(
|
||||
$"directional-shadow-terrain-commands-{terrain.BuildSequence}",
|
||||
MemoryMarshal.AsBytes(terrain.Commands),
|
||||
GpuBufferUsage.Indirect);
|
||||
}
|
||||
|
||||
IGpuBuffer? previous = _terrainCommandBuffer;
|
||||
_terrainCommandBuffer = commands;
|
||||
_terrainGpuBuildSequence = terrain.BuildSequence;
|
||||
previous?.Dispose();
|
||||
}
|
||||
|
||||
private IGpuBuffer CreateRetainedBuffer(
|
||||
string name,
|
||||
ReadOnlySpan<byte> contents,
|
||||
GpuBufferUsage usage)
|
||||
{
|
||||
if (contents.IsEmpty)
|
||||
throw new ArgumentException("Retained shadow buffers cannot be empty.", nameof(contents));
|
||||
IGpuBuffer buffer = _device.CreateBuffer(new GpuBufferDescription(
|
||||
name,
|
||||
contents.Length,
|
||||
usage | GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal));
|
||||
try
|
||||
{
|
||||
buffer.Upload(0, contents);
|
||||
return buffer;
|
||||
}
|
||||
catch
|
||||
{
|
||||
buffer.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static RetainedGpuBufferSlice Slice(IGpuBuffer? buffer) =>
|
||||
new(buffer, 0u, checked((uint)(buffer?.SizeBytes ?? 0L)));
|
||||
|
||||
private void DrawTerrain(
|
||||
IGpuPassEncoder encoder,
|
||||
in PreparedGpuUploads uploads,
|
||||
DirectionalShadowTerrainPreparedDraws draws,
|
||||
DirectionalShadowTerrainGeometry? geometry,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline? pipeline = null)
|
||||
{
|
||||
if (draws.Commands.IsEmpty)
|
||||
return;
|
||||
DirectionalShadowTerrainGeometry actual = geometry!.Value;
|
||||
encoder.BindPipeline(pipeline ?? _terrainPipeline);
|
||||
encoder.BindVertexBuffer(0, actual.VertexBuffer, 0);
|
||||
encoder.BindIndexBuffer(actual.IndexBuffer, 0, GpuIndexType.UInt32);
|
||||
GpuPushConstants push = PushForCascade(cascadeIndex, 0);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
uploads.TerrainCommands.RequireBuffer(),
|
||||
uploads.TerrainCommands.OffsetBytes,
|
||||
checked((uint)draws.Commands.Length),
|
||||
DrawCommandStride);
|
||||
}
|
||||
|
||||
private void DrawWorld(
|
||||
IGpuPassEncoder encoder,
|
||||
in PreparedGpuUploads uploads,
|
||||
DirectionalShadowPreparedDraws draws,
|
||||
DirectionalShadowMeshGeometry? geometry,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline? opaquePipeline = null,
|
||||
IGpuPipeline? cutoutPipeline = null)
|
||||
{
|
||||
if (draws.Commands.IsEmpty)
|
||||
return;
|
||||
DirectionalShadowMeshGeometry actual = geometry!.Value;
|
||||
encoder.BindStorageBuffer(
|
||||
GpuBindingModel.StorageInstances,
|
||||
uploads.Transforms.Buffer,
|
||||
uploads.Transforms.BaseOffsetBytes,
|
||||
uploads.Transforms.BindingSizeBytes);
|
||||
encoder.BindStorageBuffer(
|
||||
GpuBindingModel.StorageBatches,
|
||||
uploads.Batches.RequireBuffer(),
|
||||
uploads.Batches.OffsetBytes,
|
||||
uploads.Batches.SizeBytes);
|
||||
DrawWorldRange(
|
||||
encoder,
|
||||
uploads.WorldCommands,
|
||||
draws.OpaqueRuns,
|
||||
cascadeIndex,
|
||||
opaquePipeline ?? _worldOpaquePipeline,
|
||||
actual);
|
||||
DrawWorldRange(
|
||||
encoder,
|
||||
uploads.WorldCommands,
|
||||
draws.AlphaCutoutRuns,
|
||||
cascadeIndex,
|
||||
cutoutPipeline ?? _worldCutoutPipeline,
|
||||
actual);
|
||||
}
|
||||
|
||||
private static void DrawWorldRange(
|
||||
IGpuPassEncoder encoder,
|
||||
in RetainedGpuBufferSlice commands,
|
||||
ReadOnlySpan<DirectionalShadowPreparedRun> runs,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline pipeline,
|
||||
in DirectionalShadowMeshGeometry geometry)
|
||||
{
|
||||
if (runs.IsEmpty)
|
||||
return;
|
||||
encoder.BindPipeline(pipeline);
|
||||
encoder.BindVertexBuffer(0, geometry.VertexBuffer, 0);
|
||||
encoder.BindIndexBuffer(geometry.IndexBuffer, 0, GpuIndexType.UInt16);
|
||||
|
||||
for (int runIndex = 0; runIndex < runs.Length; runIndex++)
|
||||
{
|
||||
DirectionalShadowPreparedRun run = runs[runIndex];
|
||||
ApplyCull(encoder, run.CullMode);
|
||||
GpuPushConstants push = PushForCascade(cascadeIndex, run.StartCommand);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commands.RequireBuffer(),
|
||||
commands.OffsetBytes + checked((uint)(run.StartCommand * DrawCommandStride)),
|
||||
checked((uint)run.CommandCount),
|
||||
DrawCommandStride);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuPushConstants PushForCascade(int cascadeIndex, int drawIdOffset)
|
||||
{
|
||||
GpuPushConstants push = GpuPushConstants.Default;
|
||||
push.RenderPass = cascadeIndex;
|
||||
push.DrawIdOffset = drawIdOffset;
|
||||
return push;
|
||||
}
|
||||
|
||||
private static void ApplyCull(IGpuPassEncoder encoder, CullMode mode)
|
||||
{
|
||||
encoder.SetFrontFace(GpuFrontFace.Clockwise);
|
||||
encoder.SetCullMode(mode switch
|
||||
{
|
||||
CullMode.None => GpuCullMode.None,
|
||||
CullMode.Clockwise => GpuCullMode.Front,
|
||||
_ => GpuCullMode.Back,
|
||||
});
|
||||
}
|
||||
|
||||
private static IGpuPipeline CreatePipeline(
|
||||
IGpuDevice device,
|
||||
string name,
|
||||
GpuShaderSet shaders,
|
||||
GpuVertexLayout layout,
|
||||
GpuFrontFace frontFace,
|
||||
uint viewMask = 0) =>
|
||||
device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = name,
|
||||
Shaders = shaders,
|
||||
VertexLayout = layout,
|
||||
Topology = GpuPrimitiveTopology.TriangleList,
|
||||
Blend = GpuBlendMode.None,
|
||||
Depth = new GpuDepthState(true, true, GpuCompareOp.Less),
|
||||
Cull = GpuCullMode.Back,
|
||||
FrontFace = frontFace,
|
||||
AlphaToCoverage = false,
|
||||
ColorWrite = false,
|
||||
HasColorAttachment = false,
|
||||
AllowColorFormatVariants = false,
|
||||
SampleCount = 1,
|
||||
UsesRenderPackShaderAbi = true,
|
||||
ViewMask = viewMask,
|
||||
});
|
||||
|
||||
private (bool HasMeasurement, double Milliseconds) ResolveGpu(int cascadeCount)
|
||||
{
|
||||
if (MultiviewCascadesEnabled)
|
||||
return _device.Timers.TryResolve(MultiviewTimerName, out double measured)
|
||||
? (true, measured)
|
||||
: (false, 0d);
|
||||
double total = 0d;
|
||||
for (int i = 0; i < cascadeCount; i++)
|
||||
{
|
||||
if (!_device.Timers.TryResolve(TimerName(i), out double milliseconds))
|
||||
return (false, 0d);
|
||||
total += milliseconds;
|
||||
}
|
||||
return (true, total);
|
||||
}
|
||||
|
||||
private DirectionalSunShadowDiagnostics Disabled(
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
DirectionalSunShadowCpuStageTicks cpuStages = default) =>
|
||||
new(
|
||||
environment.Reason,
|
||||
0f,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0d,
|
||||
0d,
|
||||
false,
|
||||
_quality.ApproximateDepthMapBytes,
|
||||
cpuStages,
|
||||
SourceKind: environment.SourceKind,
|
||||
SourceObjectIndex: environment.SourceObjectIndex,
|
||||
SourceGfxObjId: environment.SourceGfxObjId,
|
||||
SurfaceToLightDirection: environment.SurfaceToLightDirection,
|
||||
LightElevationSin: environment.LightElevationSin);
|
||||
|
||||
private void EnsureBatchCapacity(int required)
|
||||
{
|
||||
if (_batchScratch.Length >= required)
|
||||
return;
|
||||
int capacity = _batchScratch.Length == 0 ? 16 : _batchScratch.Length;
|
||||
while (capacity < required)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref _batchScratch, capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
_worldCutoutPipeline.Dispose();
|
||||
_worldCutoutMultiviewPipeline?.Dispose();
|
||||
_worldOpaqueMultiviewPipeline?.Dispose();
|
||||
_terrainMultiviewPipeline?.Dispose();
|
||||
_worldOpaquePipeline.Dispose();
|
||||
_terrainPipeline.Dispose();
|
||||
_terrainCommandBuffer?.Dispose();
|
||||
_worldCommandBuffer?.Dispose();
|
||||
_worldBatchBuffer?.Dispose();
|
||||
_transformBuffers.Dispose();
|
||||
_device.ReleaseTextureSlot(_textureSlot);
|
||||
_sampler.Dispose();
|
||||
_target.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private readonly record struct DirectionalShadowBatchGpuData(
|
||||
uint TextureIndex,
|
||||
uint Reserved,
|
||||
uint TextureLayer,
|
||||
uint Flags);
|
||||
|
||||
private readonly record struct RetainedGpuBufferSlice(
|
||||
IGpuBuffer? Buffer,
|
||||
uint OffsetBytes,
|
||||
uint SizeBytes)
|
||||
{
|
||||
internal IGpuBuffer RequireBuffer() => Buffer
|
||||
?? throw new InvalidOperationException(
|
||||
"A non-empty directional-shadow draw has no retained GPU buffer.");
|
||||
}
|
||||
|
||||
private readonly record struct PreparedGpuUploads(
|
||||
WorldTransformFrameSlice transforms,
|
||||
RetainedGpuBufferSlice batches,
|
||||
RetainedGpuBufferSlice worldCommands,
|
||||
RetainedGpuBufferSlice terrainCommands)
|
||||
{
|
||||
internal WorldTransformFrameSlice Transforms { get; } = transforms;
|
||||
internal RetainedGpuBufferSlice Batches { get; } = batches;
|
||||
internal RetainedGpuBufferSlice WorldCommands { get; } = worldCommands;
|
||||
internal RetainedGpuBufferSlice TerrainCommands { get; } = terrainCommands;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,43 @@ public sealed class GameWindow :
|
|||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
/ (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
|
||||
internal static WindowOptions CreateStartupWindowOptions(
|
||||
bool exactAutomationFramebuffer,
|
||||
string persistedResolution,
|
||||
bool useVSync)
|
||||
{
|
||||
WindowOptions defaults = WindowOptions.DefaultVulkan;
|
||||
Vector2D<int> size = new(1280, 720);
|
||||
WindowBorder border = defaults.WindowBorder;
|
||||
if (exactAutomationFramebuffer)
|
||||
{
|
||||
if (!SilkRuntimeDisplayWindowTarget.TryParseResolution(
|
||||
persistedResolution,
|
||||
out int width,
|
||||
out int height))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Exact automation framebuffer requires a valid persisted resolution.");
|
||||
}
|
||||
size = new Vector2D<int>(width, height);
|
||||
border = WindowBorder.Hidden;
|
||||
}
|
||||
|
||||
return defaults with
|
||||
{
|
||||
Size = size,
|
||||
Title = "acdream — Vulkan",
|
||||
VSync = useVSync,
|
||||
WindowBorder = border,
|
||||
// A desktop-sized borderless automation window must stay hidden,
|
||||
// not iconified. Windows throttles/occludes an iconified GLFW
|
||||
// surface, which prevents the performance gate from collecting a
|
||||
// complete rolling sample window. Ordinary launches retain the
|
||||
// Silk default visibility.
|
||||
IsVisible = !exactAutomationFramebuffer,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly AcDream.App.RuntimeOptions _options;
|
||||
// Campaign LA slice LA1: no-op instance when --session-config didn't
|
||||
// configure a statusFile (or the env-var launch path was used at all).
|
||||
|
|
@ -57,6 +94,13 @@ public sealed class GameWindow :
|
|||
// loop!" and would otherwise bury whatever exception actually wounded
|
||||
// the loop). See docs/ISSUES.md #343.
|
||||
private bool _renderLoopArmed;
|
||||
// Silk may invoke Closing synchronously from IWindow.Close during Update,
|
||||
// then still invoke Render once before its loop exits. Teardown cannot run
|
||||
// from that Closing callback: it would dispose the scene while the cached
|
||||
// render delegate is still eligible to execute. Latch the edge, skip that
|
||||
// terminal render, and close the ownership graph after Run returns.
|
||||
private bool _nativeCloseRequested;
|
||||
private bool _nativeRunReturned;
|
||||
private SilkWindowCallbackBinding? _windowCallbacks;
|
||||
private GameWindowGraphics? _graphics;
|
||||
// Campaign V slice V6h: borrowed, not owned — _graphics owns the context and
|
||||
|
|
@ -414,6 +458,8 @@ public sealed class GameWindow :
|
|||
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
||||
private InteractionUiLateBindings? _interactionUiLateBindings;
|
||||
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
|
||||
private readonly AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource
|
||||
_renderPackDiagnostics = new();
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.RuntimeCombatTargetOperationsSlot
|
||||
|
|
@ -449,6 +495,7 @@ public sealed class GameWindow :
|
|||
private AcDream.App.Rendering.ChargenPreviewController? _summaryPreviewController;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
private readonly AcDream.App.Plugins.BufferedRenderPackRegistry? _renderPackRegistry;
|
||||
private AcDream.App.Plugins.GraphicalPluginSession? _pluginSession;
|
||||
// Campaign V slice V11 deleted the ImGui developer-tools frontend along
|
||||
// with the OpenGL backend it required, so no host ever composes a
|
||||
|
|
@ -636,7 +683,8 @@ public sealed class GameWindow :
|
|||
WorldEvents worldEvents,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry,
|
||||
GraphicalHostPlatformServices platformServices,
|
||||
AcDream.App.Plugins.AppAutomationSurface? automation = null)
|
||||
AcDream.App.Plugins.AppAutomationSurface? automation = null,
|
||||
AcDream.App.Plugins.BufferedRenderPackRegistry? renderPackRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_automation = automation;
|
||||
|
|
@ -724,6 +772,7 @@ public sealed class GameWindow :
|
|||
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
|
||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||
_uiRegistry = uiRegistry;
|
||||
_renderPackRegistry = renderPackRegistry;
|
||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
||||
_liveEntityRuntimeSlot);
|
||||
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
||||
|
|
@ -799,12 +848,10 @@ public sealed class GameWindow :
|
|||
// attribute there — both are attachment properties the RHI device
|
||||
// configures instead. The raw-GL window options this used to fork to
|
||||
// were deleted at Campaign V slice V11.
|
||||
var options = WindowOptions.DefaultVulkan with
|
||||
{
|
||||
Size = new Vector2D<int>(1280, 720),
|
||||
Title = "acdream — Vulkan",
|
||||
VSync = startupPacing.UseVSync,
|
||||
};
|
||||
WindowOptions options = CreateStartupWindowOptions(
|
||||
_options.ExactAutomationFramebuffer,
|
||||
startup.Display.Resolution,
|
||||
startupPacing.UseVSync);
|
||||
_startupPacing = startupPacing;
|
||||
_startupQuality = startup.Quality;
|
||||
|
||||
|
|
@ -833,6 +880,8 @@ public sealed class GameWindow :
|
|||
try
|
||||
{
|
||||
_window.Run();
|
||||
_nativeRunReturned = true;
|
||||
CompleteShutdown(releaseNativeWindow: false);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
|
|
@ -1359,7 +1408,10 @@ public sealed class GameWindow :
|
|||
_localPlayerMode,
|
||||
_chaseCameraInput,
|
||||
_pointerPosition,
|
||||
_renderDiagnosticLog),
|
||||
_renderDiagnosticLog,
|
||||
_options.InitialOrbitDistanceMeters,
|
||||
_options.InitialOrbitYawDegrees,
|
||||
_options.InitialOrbitPitchDegrees),
|
||||
this).Compose(platformResult),
|
||||
(platformResult, hostInputCamera) =>
|
||||
new ContentEffectsAudioCompositionPhase(
|
||||
|
|
@ -1387,7 +1439,11 @@ public sealed class GameWindow :
|
|||
new SilkRuntimeDisplayWindowTarget(_window!),
|
||||
_displayFramePacing,
|
||||
hostInputCamera.CameraController,
|
||||
contentEffectsAudio.Audio?.Engine)))
|
||||
contentEffectsAudio.Audio?.Engine))
|
||||
{
|
||||
RenderPacks = _renderPackRegistry,
|
||||
GpuDevice = hostInputCamera.GpuDevice,
|
||||
})
|
||||
.Compose(platformResult, hostInputCamera, contentEffectsAudio),
|
||||
(platformResult, contentEffectsAudio, settingsDevTools) =>
|
||||
{
|
||||
|
|
@ -1461,7 +1517,9 @@ public sealed class GameWindow :
|
|||
Console.WriteLine,
|
||||
hostInputCamera.GpuDevice,
|
||||
hostInputCamera.GpuFrameLifetime,
|
||||
() => WorldTime.CurrentCalendar),
|
||||
() => WorldTime.CurrentCalendar,
|
||||
settingsDevTools.RenderPacks,
|
||||
_renderPackDiagnostics.CaptureDiagnostics),
|
||||
_retailUiLease,
|
||||
this).Compose(
|
||||
platformResult,
|
||||
|
|
@ -1519,7 +1577,8 @@ public sealed class GameWindow :
|
|||
DevFrameDiagnostics: null,
|
||||
_uiFrameDiagnostics,
|
||||
Console.WriteLine,
|
||||
compositionToast),
|
||||
compositionToast,
|
||||
_renderPackDiagnostics),
|
||||
this).Compose(
|
||||
platformResult,
|
||||
hostInputCamera,
|
||||
|
|
@ -1631,7 +1690,8 @@ public sealed class GameWindow :
|
|||
_animatedEntities,
|
||||
_updateFrameClock,
|
||||
_frameGraphs,
|
||||
Console.WriteLine),
|
||||
Console.WriteLine,
|
||||
_renderPackDiagnostics),
|
||||
this).Compose(
|
||||
platformResult,
|
||||
hostInputCamera,
|
||||
|
|
@ -1672,6 +1732,11 @@ public sealed class GameWindow :
|
|||
// #343: see OnUpdate above — armed on entry, cleared on every normal
|
||||
// exit path below, left stuck true if anything here throws.
|
||||
_renderLoopArmed = true;
|
||||
if (_nativeCloseRequested)
|
||||
{
|
||||
_renderLoopArmed = false;
|
||||
return;
|
||||
}
|
||||
Vector2D<int> size = _window!.Size;
|
||||
// Campaign V slice V6h: swapchain currency is the one piece of
|
||||
// presentation the RHI contract deliberately leaves to the host (plan
|
||||
|
|
@ -1735,12 +1800,23 @@ public sealed class GameWindow :
|
|||
|
||||
private void CompleteShutdown(bool releaseNativeWindow)
|
||||
{
|
||||
// IWindow.Close can raise Closing synchronously from Update and Silk
|
||||
// can still issue one cached Render callback before Run returns. Keep
|
||||
// Closing as the one narrow shutdown edge, but do not release frame
|
||||
// owners until the native loop has actually returned. OnRender sees
|
||||
// this latch and makes that terminal callback inert.
|
||||
if (!releaseNativeWindow && !_nativeRunReturned)
|
||||
{
|
||||
_nativeCloseRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_lifetime.HasShutdownRoots)
|
||||
{
|
||||
// Campaign LA slice LA1: capture BEFORE the shutdown roots run —
|
||||
// by the time teardown completes, IsInWorld is always false
|
||||
// regardless of whether a real session was ever connected.
|
||||
// OnClosing() and Dispose() both funnel through this method;
|
||||
// post-Run shutdown and Dispose() both funnel through this method;
|
||||
// HasShutdownRoots's own guard means this fires exactly once,
|
||||
// from whichever of the two reaches it first.
|
||||
if (_runtime.Session.IsInWorld)
|
||||
|
|
@ -1754,7 +1830,7 @@ public sealed class GameWindow :
|
|||
if (report.Status == GameWindowLifetimeStatus.Complete)
|
||||
{
|
||||
// "exited" = terminal — only the true Dispose() call (not the
|
||||
// OnClosing() native-window-close-request pass) represents the
|
||||
// post-Run native-window-close-request pass) represents the
|
||||
// process actually being done.
|
||||
if (releaseNativeWindow)
|
||||
ReportExited(report);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -60,16 +62,20 @@ internal static class GpuBindingModel
|
|||
/// <summary>Retail SmartBox selection lighting: one vec2 (luminosity, diffuse) per instance.</summary>
|
||||
public const uint StorageInstanceSelectionLighting = 8;
|
||||
|
||||
// Campaign V slice V11 deleted StorageTextureTable (binding 9): the GL-only
|
||||
// emulation of the Vulkan texture table via a storage buffer of uvec2
|
||||
// bindless handles indexed by GpuTextureSlot.Index. The Vulkan backend
|
||||
// always bound TextureTableSet instead and never used this binding — every
|
||||
// Vulkan descriptor set layout declared it anyway (seeded with a dummy
|
||||
// buffer, like every other unused-by-a-given-shader binding), purely
|
||||
// because it counted toward StorageBindingCount.
|
||||
/// <summary>
|
||||
/// #226 per-instance retail detail category. One uint parallel to
|
||||
/// <see cref="StorageInstances"/>: 1 = building shell, 0 = every other
|
||||
/// object. EnvCell detail uses its renderer-wide category and does not
|
||||
/// inspect this field.
|
||||
/// </summary>
|
||||
public const uint StorageInstanceDetailCategory = 9;
|
||||
|
||||
// Campaign V slice V11 deleted the old GL-only StorageTextureTable from
|
||||
// binding 9. #226 deliberately reclaims that vacant number for the detail
|
||||
// category above; the Vulkan texture table remains set 2.
|
||||
|
||||
/// <summary>One past the highest storage binding — the count the backend must support.</summary>
|
||||
public const uint StorageBindingCount = 9;
|
||||
public const uint StorageBindingCount = 10;
|
||||
|
||||
// ---- set 1: uniform buffers ----
|
||||
|
||||
|
|
@ -106,9 +112,43 @@ internal static class GpuBindingModel
|
|||
/// </summary>
|
||||
public const uint UniformSkyParams = 4;
|
||||
|
||||
/// <summary>Set index carrying every uniform buffer.</summary>
|
||||
/// <summary>
|
||||
/// Immutable authored-atmosphere inputs for one enhanced world frame in
|
||||
/// opt-in render-pack descriptor set 3.
|
||||
/// The std140 ABI is four vec4 values: sunScreen, sunColor, viewport, and
|
||||
/// weather. See AtmosphericFrameUniforms and atmospheric_common.glsl.
|
||||
/// </summary>
|
||||
public const uint UniformAtmosphericFrame = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Directional-shadow cascade matrices and sampling parameters. Reserved by
|
||||
/// the shared pack ABI even when a Tier-1 graph leaves the dummy binding in
|
||||
/// place, so Tier 2 never changes the common pipeline layout.
|
||||
/// </summary>
|
||||
public const uint UniformDirectionalShadow = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Per-fullscreen-pass values for enhancement graphs. The v1 std140 ABI is
|
||||
/// four vec4 values named params0..params3; individual passes assign their
|
||||
/// meanings without changing the descriptor layout.
|
||||
/// </summary>
|
||||
public const uint UniformPackPass = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Pack-declared settings in declaration order: sixteen std140 vec4 values
|
||||
/// (64 scalar slots). Preset overrides are resolved before activation.
|
||||
/// </summary>
|
||||
public const uint UniformPackSettings = RenderPackShaderAbi.PackSettingsBinding;
|
||||
|
||||
/// <summary>Set index carrying retail uniform buffers.</summary>
|
||||
public const uint UniformSet = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in render-pack uniform set. It is absent from retail layouts and is
|
||||
/// created only while a render-pack pipeline is alive.
|
||||
/// </summary>
|
||||
public const uint RenderPackUniformSet = RenderPackShaderAbi.UniformDescriptorSet;
|
||||
|
||||
// ---- set 2: the global texture table ----
|
||||
|
||||
/// <summary>Set index of the sampled-texture descriptor array.</summary>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ internal sealed record GpuCapabilityRecord
|
|||
/// <summary>Required alignment for a storage-buffer binding offset.</summary>
|
||||
public required uint MinStorageBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Largest byte range one storage-buffer descriptor may expose. Vulkan
|
||||
/// guarantees at least 128 MiB; optional render packs use the exact probed
|
||||
/// value to size scene-dependent buffers instead of imposing a host ceiling.
|
||||
/// </summary>
|
||||
public uint MaxStorageBufferRangeBytes { get; init; } = 128u * 1024u * 1024u;
|
||||
|
||||
/// <summary>Required alignment for a uniform-buffer binding offset.</summary>
|
||||
public required uint MinUniformBufferOffsetAlignment { get; init; }
|
||||
|
||||
|
|
@ -43,6 +50,20 @@ internal sealed record GpuCapabilityRecord
|
|||
/// <summary>Highest supported multisample count for the backbuffer.</summary>
|
||||
public required uint MaxSampleCount { get; init; }
|
||||
|
||||
/// <summary>Largest supported two-dimensional image edge from the selected adapter.</summary>
|
||||
public required uint MaxImageDimension2D { get; init; }
|
||||
|
||||
/// <summary>Largest supported image-array layer count from the selected adapter.</summary>
|
||||
public required uint MaxImageArrayLayers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes in device-local heaps on the selected adapter. Render-pack
|
||||
/// policy derives a deliberately bounded share from this value before any
|
||||
/// optional image is allocated; zero means that no optional pack memory may
|
||||
/// be assumed.
|
||||
/// </summary>
|
||||
public required ulong DeviceLocalMemoryBytes { get; init; }
|
||||
|
||||
/// <summary>Multi-draw-indirect. Mandatory — it is the entire draw architecture.</summary>
|
||||
public required bool SupportsMultiDrawIndirect { get; init; }
|
||||
|
||||
|
|
@ -62,6 +83,27 @@ internal sealed record GpuCapabilityRecord
|
|||
/// </summary>
|
||||
public required bool SupportsPersistentlyMappedRings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether RGBA16F images can be colour attachments, sampled, and linearly
|
||||
/// filtered. Optional: absence disables HDR packs, never the retail client.
|
||||
/// </summary>
|
||||
public required bool SupportsRgba16FloatRenderTargets { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Highest usable sample count for an RGBA16F colour attachment that is
|
||||
/// also a sampled resolve target. Zero means the format is unavailable.
|
||||
/// </summary>
|
||||
public required uint MaxRgba16FloatSampleCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the selected combined depth/stencil format can also expose its
|
||||
/// depth aspect as a sampled image. Optional: needed by screen-space packs.
|
||||
/// </summary>
|
||||
public required bool SupportsSampledDepth { get; init; }
|
||||
|
||||
/// <summary>Vulkan core multiview; optional and used only by packs that declare it.</summary>
|
||||
public required bool SupportsMultiview { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Every mandatory capability this device fails to provide, phrased as
|
||||
/// operator-facing sentences. Empty means the device can run acdream.
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ internal enum GpuRingUsage
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Texture formats acdream actually produces from DAT surfaces. BC1/2/3 are the
|
||||
/// DXT1/3/5 compressed surfaces uploaded verbatim; RGBA8 covers decoded and
|
||||
/// composited art; R8 is the stb-baked font atlas.
|
||||
/// Texture formats acdream uploads or renders. BC1/2/3 are the DXT1/3/5 DAT
|
||||
/// surfaces uploaded verbatim; RGBA8 covers decoded and composited art; R8 is
|
||||
/// the stb-baked font atlas; RGBA16F is reserved for opt-in HDR intermediates.
|
||||
/// </summary>
|
||||
internal enum GpuTextureFormat
|
||||
{
|
||||
|
|
@ -72,6 +72,12 @@ internal enum GpuTextureFormat
|
|||
/// <summary>Colour attachment format for offscreen targets (paperdoll, appraisal).</summary>
|
||||
Rgba8UnormRenderTarget,
|
||||
|
||||
/// <summary>
|
||||
/// Half-float HDR colour attachment used only by opt-in enhancement graphs.
|
||||
/// The retail/default graph remains on <see cref="Rgba8UnormRenderTarget"/>.
|
||||
/// </summary>
|
||||
Rgba16FloatRenderTarget,
|
||||
|
||||
/// <summary>Combined depth+stencil attachment. #117's portal punch needs the stencil aspect.</summary>
|
||||
Depth24Stencil8,
|
||||
}
|
||||
|
|
@ -129,6 +135,14 @@ internal enum GpuBlendMode
|
|||
/// `ParticleRenderer` needs it too (slice V4e).
|
||||
/// </summary>
|
||||
InverseAlpha,
|
||||
|
||||
/// <summary>
|
||||
/// Retail building/EnvCell detail overlay:
|
||||
/// <c>DstColor, OneMinusSrcAlpha</c>. This intentionally preserves the
|
||||
/// retail client's measured brightening; it is not a conventional
|
||||
/// modulate/roughening blend.
|
||||
/// </summary>
|
||||
RetailDetail,
|
||||
}
|
||||
|
||||
internal enum GpuCompareOp
|
||||
|
|
|
|||
|
|
@ -21,20 +21,24 @@ internal readonly record struct GpuColorAttachment(
|
|||
Vector4 ClearColor);
|
||||
|
||||
/// <summary>
|
||||
/// The depth/stencil attachment for a pass. Depth is transient in every acdream
|
||||
/// pass — nothing reads it after the frame — so <see cref="Store"/> is normally
|
||||
/// <see cref="GpuStoreOp.DontCare"/>, which lets Vulkan skip writing it back to
|
||||
/// memory entirely.
|
||||
/// The depth/stencil attachment for a pass. Ordinary world/private-viewport
|
||||
/// depth is transient, so <see cref="Store"/> is normally
|
||||
/// <see cref="GpuStoreOp.DontCare"/>. Directional shadow layers instead name a
|
||||
/// <see cref="DirectionalTarget"/> and use Store so receivers may sample them.
|
||||
/// </summary>
|
||||
/// <param name="Load">What happens to existing contents on entry.</param>
|
||||
/// <param name="Store">What happens to contents on exit.</param>
|
||||
/// <param name="ClearDepth">Depth clear value. acdream renders with NDC z in [0,1], so far = 1.</param>
|
||||
/// <param name="ClearStencil">Stencil clear value; #117's portal punch uses the stencil aspect.</param>
|
||||
/// <param name="DirectionalTarget">Dedicated layered depth target, or null for the pass colour target/backbuffer depth.</param>
|
||||
/// <param name="Layer">The cascade layer when <paramref name="DirectionalTarget"/> is present.</param>
|
||||
internal readonly record struct GpuDepthAttachment(
|
||||
GpuLoadOp Load,
|
||||
GpuStoreOp Store,
|
||||
float ClearDepth,
|
||||
uint ClearStencil);
|
||||
uint ClearStencil,
|
||||
IGpuDirectionalDepthTarget? DirectionalTarget = null,
|
||||
int Layer = 0);
|
||||
|
||||
/// <summary>
|
||||
/// One rendering pass: a set of attachments, their load/store behaviour, and the
|
||||
|
|
@ -57,15 +61,21 @@ internal sealed record GpuPassDescription
|
|||
/// <summary>Stable identifier, surfaced as a debug label in captures.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>The colour attachment. Required — acdream has no colour-less passes.</summary>
|
||||
/// <summary>The colour attachment. Ignored when <see cref="HasColorAttachment"/> is false.</summary>
|
||||
public required GpuColorAttachment Color { get; init; }
|
||||
|
||||
/// <summary>False only for dedicated depth-only producers such as directional shadow maps.</summary>
|
||||
public bool HasColorAttachment { get; init; } = true;
|
||||
|
||||
/// <summary>Depth/stencil attachment, or null for 2-D passes that need no depth.</summary>
|
||||
public GpuDepthAttachment? Depth { get; init; }
|
||||
|
||||
/// <summary>Samples per pixel. Must equal <see cref="GpuPipelineDescription.SampleCount"/> of every pipeline bound inside.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
|
||||
/// <summary>Non-zero Vulkan multiview mask. Ordinary passes always leave this zero.</summary>
|
||||
public uint ViewMask { get; init; }
|
||||
|
||||
/// <summary>Clears colour and depth to the standard frame-start values against the backbuffer.</summary>
|
||||
public static GpuPassDescription BackbufferClear(string name, Vector4 clearColor, int sampleCount) => new()
|
||||
{
|
||||
|
|
@ -82,4 +92,43 @@ internal sealed record GpuPassDescription
|
|||
ClearStencil: 0),
|
||||
SampleCount = sampleCount,
|
||||
};
|
||||
|
||||
/// <summary>Clears and stores one cascade layer of a directional-depth array.</summary>
|
||||
public static GpuPassDescription DirectionalDepth(
|
||||
string name,
|
||||
IGpuDirectionalDepthTarget target,
|
||||
int layer) => new()
|
||||
{
|
||||
Name = name,
|
||||
Color = default,
|
||||
HasColorAttachment = false,
|
||||
Depth = new GpuDepthAttachment(
|
||||
Load: GpuLoadOp.Clear,
|
||||
Store: GpuStoreOp.Store,
|
||||
ClearDepth: 1f,
|
||||
ClearStencil: 0,
|
||||
DirectionalTarget: target,
|
||||
Layer: layer),
|
||||
SampleCount = 1,
|
||||
};
|
||||
|
||||
/// <summary>Clears and stores all contiguous cascade layers in one multiview pass.</summary>
|
||||
public static GpuPassDescription DirectionalDepthMultiview(
|
||||
string name,
|
||||
IGpuDirectionalDepthTarget target,
|
||||
uint viewMask) => new()
|
||||
{
|
||||
Name = name,
|
||||
Color = default,
|
||||
HasColorAttachment = false,
|
||||
Depth = new GpuDepthAttachment(
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
1f,
|
||||
0,
|
||||
target,
|
||||
Layer: 0),
|
||||
SampleCount = 1,
|
||||
ViewMask = viewMask,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,13 +168,39 @@ internal sealed record GpuVertexLayout(
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names one GLSL shader pair. The backend resolves it: the GL backend loads
|
||||
/// <c>Rendering/Shaders/{Name}.vert</c> and <c>.frag</c> and compiles at startup;
|
||||
/// the Vulkan backend loads the committed <c>Rendering/Shaders/spv/{Name}.vert.spv</c>
|
||||
/// and <c>.frag.spv</c> produced by <c>tools/compile-shaders.ps1</c>. One source
|
||||
/// of truth (the GLSL), two consumption paths.
|
||||
/// Names one SPIR-V shader pair. Renderer-owned shaders resolve from the
|
||||
/// committed shader directory. A selected render pack instead supplies an
|
||||
/// immutable candidate-owned byte pair, so validation never turns into a
|
||||
/// second host-path lookup or a private built-in shortcut.
|
||||
/// </summary>
|
||||
internal readonly record struct GpuShaderSet(string Name);
|
||||
internal readonly record struct GpuShaderSet
|
||||
{
|
||||
internal GpuShaderSet(string name)
|
||||
: this(name, ReadOnlyMemory<byte>.Empty, ReadOnlyMemory<byte>.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
internal GpuShaderSet(
|
||||
string name,
|
||||
ReadOnlyMemory<byte> vertexSpirv,
|
||||
ReadOnlyMemory<byte> fragmentSpirv)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
if (vertexSpirv.IsEmpty != fragmentSpirv.IsEmpty)
|
||||
throw new ArgumentException("Both SPIR-V stages must be supplied together.");
|
||||
Name = name;
|
||||
VertexSpirv = vertexSpirv;
|
||||
FragmentSpirv = fragmentSpirv;
|
||||
}
|
||||
|
||||
internal string Name { get; }
|
||||
|
||||
internal ReadOnlyMemory<byte> VertexSpirv { get; }
|
||||
|
||||
internal ReadOnlyMemory<byte> FragmentSpirv { get; }
|
||||
|
||||
internal bool HasEmbeddedSpirv => !VertexSpirv.IsEmpty;
|
||||
}
|
||||
|
||||
/// <summary>Depth-buffer behaviour baked into a pipeline.</summary>
|
||||
/// <param name="Test">Whether depth testing is enabled at all.</param>
|
||||
|
|
@ -241,6 +267,8 @@ internal readonly record struct GpuStencilState(
|
|||
/// </summary>
|
||||
internal sealed record GpuPipelineDescription
|
||||
{
|
||||
/// <summary>Non-zero only for a pipeline compiled for a matching multiview pass.</summary>
|
||||
public uint ViewMask { get; init; }
|
||||
/// <summary>Stable identifier, e.g. <c>"mesh-opaque"</c>. Surfaced to RenderDoc and validation layers.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
|
|
@ -275,6 +303,12 @@ internal sealed record GpuPipelineDescription
|
|||
/// <summary>Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.</summary>
|
||||
public bool ColorWrite { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the compatible dynamic-rendering pass carries a colour
|
||||
/// attachment. False creates a true depth-only graphics pipeline.
|
||||
/// </summary>
|
||||
public bool HasColorAttachment { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this pipeline uses the stencil aspect at all.
|
||||
///
|
||||
|
|
@ -324,6 +358,21 @@ internal sealed record GpuPipelineDescription
|
|||
/// </summary>
|
||||
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an opt-in graph may prebuild this pipeline against an additional
|
||||
/// colour-attachment format. World pipelines leave this enabled; dedicated
|
||||
/// fullscreen pipelines already name their only format and disable it.
|
||||
/// </summary>
|
||||
public bool AllowColorFormatVariants { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Opts this pipeline into render-pack shader ABI v1. Vulkan then uses the
|
||||
/// lazy four-set pipeline layout whose set 3 contains bindings 5..8; retail
|
||||
/// pipelines keep the authoritative three-set layout and create no pack
|
||||
/// descriptors or layouts.
|
||||
/// </summary>
|
||||
public bool UsesRenderPackShaderAbi { get; init; }
|
||||
|
||||
/// <summary>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ internal struct GpuPushConstants
|
|||
/// </summary>
|
||||
public uint TextureIndexA;
|
||||
|
||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot — currently the terrain alpha-mask array.</summary>
|
||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot; terrain
|
||||
/// uses it for the alpha-mask array, while shared-pose world/detail passes
|
||||
/// carry the absolute transform-prefix instance count.</summary>
|
||||
public uint TextureIndexB;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -76,22 +76,55 @@ internal readonly record struct GpuSamplerDescription(
|
|||
GpuAddressMode.ClampToEdge,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
MaxAnisotropy: 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Discrete nearest-clamp depth reads for manual PCF. Mip-nearest is
|
||||
/// deliberate even though the shadow image has one level: it keeps this
|
||||
/// pack-owned sampler distinct from the device's long-lived UI sampler.
|
||||
/// </summary>
|
||||
public static GpuSamplerDescription ShadowNearestClamp { get; } = new(
|
||||
GpuFilter.Nearest,
|
||||
GpuFilter.Nearest,
|
||||
GpuMipFilter.Nearest,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
MaxAnisotropy: 1f);
|
||||
}
|
||||
|
||||
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking.</summary>
|
||||
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking, or an enhancement intermediate.</summary>
|
||||
/// <param name="Name">Stable identifier for debug tooling.</param>
|
||||
/// <param name="Width">Colour attachment width in pixels.</param>
|
||||
/// <param name="Height">Colour attachment height in pixels.</param>
|
||||
/// <param name="ColorFormat">Colour attachment format.</param>
|
||||
/// <param name="DepthFormat">Depth/stencil format, or null for a colour-only target.</param>
|
||||
/// <param name="SampleCount">1 for single-sampled. Offscreen targets stay single-sampled.</param>
|
||||
/// <param name="SampleCount">
|
||||
/// Attachment sample count. Values above one use transient multisample
|
||||
/// attachments and resolve into the single-sampled textures exposed by
|
||||
/// <see cref="IGpuRenderTarget"/>.
|
||||
/// </param>
|
||||
/// <param name="SampleableDepth">
|
||||
/// Whether the depth result must be exposed for later shader sampling. This is
|
||||
/// opt-in so ordinary private viewports retain transient attachment-only depth.
|
||||
/// </param>
|
||||
internal readonly record struct GpuRenderTargetDescription(
|
||||
string Name,
|
||||
int Width,
|
||||
int Height,
|
||||
GpuTextureFormat ColorFormat,
|
||||
GpuTextureFormat? DepthFormat,
|
||||
int SampleCount);
|
||||
int SampleCount,
|
||||
bool SampleableDepth = false);
|
||||
|
||||
/// <summary>
|
||||
/// A single-sampled, sampleable depth-array used by directional shadow maps.
|
||||
/// Each cascade is rendered through its own 2-D layer attachment while the
|
||||
/// complete array is registered once in the global texture table.
|
||||
/// </summary>
|
||||
internal readonly record struct GpuDirectionalDepthTargetDescription(
|
||||
string Name,
|
||||
int Resolution,
|
||||
int LayerCount,
|
||||
GpuTextureFormat DepthFormat = GpuTextureFormat.Depth24Stencil8);
|
||||
|
||||
/// <summary>
|
||||
/// A slot in the device's global texture table — the backend-neutral replacement
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ internal interface IGpuBuffer : IDisposable
|
|||
GpuBufferUsage Usage { get; }
|
||||
GpuMemoryResidency Residency { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True when CPU writes through a mapped HostWritable allocation are made
|
||||
/// available without an explicit non-coherent atom flush. Retained mapped
|
||||
/// resources may require this and fail safe when a device cannot provide it.
|
||||
/// </summary>
|
||||
bool HostWritesAreCoherent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/>. On a
|
||||
/// <see cref="GpuMemoryResidency.DeviceLocal"/> buffer this stages through a
|
||||
|
|
@ -89,8 +96,32 @@ internal interface IGpuRenderTarget : IDisposable
|
|||
{
|
||||
GpuRenderTargetDescription Description { get; }
|
||||
|
||||
/// <summary>The colour attachment, for registering into the texture table or blitting into UI.</summary>
|
||||
/// <summary>
|
||||
/// The single-sampled colour result, for registering into the texture table
|
||||
/// or blitting into UI. A multisampled target resolves into this texture;
|
||||
/// callers never sample its transient multisample attachment directly.
|
||||
/// </summary>
|
||||
IGpuTexture ColorTexture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The single-sampled depth result when
|
||||
/// <see cref="GpuRenderTargetDescription.SampleableDepth"/> was requested;
|
||||
/// otherwise null. Combined depth/stencil targets expose the depth aspect
|
||||
/// only through the sampled view while retaining stencil for rendering.
|
||||
/// </summary>
|
||||
IGpuTexture? DepthTexture { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sampleable directional-depth array. Layers are attachment-addressable by
|
||||
/// <see cref="GpuDepthAttachment.Layer"/>; callers sample the full array through
|
||||
/// <see cref="DepthTexture"/> after the producing passes end.
|
||||
/// </summary>
|
||||
internal interface IGpuDirectionalDepthTarget : IDisposable
|
||||
{
|
||||
GpuDirectionalDepthTargetDescription Description { get; }
|
||||
|
||||
IGpuTexture DepthTexture { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -106,4 +137,12 @@ internal interface IGpuTimerPool
|
|||
|
||||
/// <summary>Milliseconds measured for <paramref name="scopeName"/> in the most recent retired frame.</summary>
|
||||
bool TryResolve(string scopeName, out double milliseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the newest retired measurement for <paramref name="scopeName"/>.
|
||||
/// Distribution builders use this form so a GPU result is sampled exactly
|
||||
/// once even when the render thread runs several frames before another
|
||||
/// flight slot retires.
|
||||
/// </summary>
|
||||
bool TryTakeResolved(string scopeName, out double milliseconds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ namespace AcDream.App.Rendering.Gpu;
|
|||
/// The RHI root: creates every GPU resource, owns the global texture table, and
|
||||
/// drives the frame loop. One instance per graphics context, constructed during
|
||||
/// composition and threaded into renderers in place of the raw <c>GL</c> handle.
|
||||
/// Resource creation/registration and retirement are safe for one asynchronous
|
||||
/// off-side render-pack preparation worker while the render thread records the
|
||||
/// active generation. Frame/pass recording and queued-device-action draining
|
||||
/// remain render-thread-only.
|
||||
///
|
||||
/// Campaign V (see <c>docs/plans/2026-07-27-vulkan-campaign.md</c>) implements
|
||||
/// this interface twice: first on OpenGL — behaviour-preserving, so each renderer
|
||||
|
|
@ -49,6 +53,10 @@ internal interface IGpuDevice : IDisposable
|
|||
|
||||
IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description);
|
||||
|
||||
/// <summary>Creates the dedicated 2-4 cascade sampleable depth array.</summary>
|
||||
IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
|
||||
in GpuDirectionalDepthTargetDescription description);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a (texture, sampler) pair into the global table and returns the
|
||||
/// slot shaders index it by. The same texture registered with two samplers
|
||||
|
|
|
|||
|
|
@ -65,6 +65,14 @@ internal interface IGpuFrame : IDisposable
|
|||
/// </summary>
|
||||
GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes CPU writes made through a retained host-writable storage
|
||||
/// buffer before a later pass reads them in a shader. Frame-ring writes use
|
||||
/// the frame submission's existing visibility contract; this explicit seam
|
||||
/// exists for pack-owned mapped buffers that persist across submissions.
|
||||
/// </summary>
|
||||
void PublishHostStorageWrites(IGpuBuffer buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a rendering pass. The returned encoder must be disposed before the
|
||||
/// next pass begins; nesting is not supported and no acdream pass needs it.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace AcDream.App.Rendering.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// Device-owned lifetime for attachment-format variants of already-created
|
||||
/// graphics pipelines. Vulkan dynamic rendering bakes the colour format into a
|
||||
/// pipeline; an enhancement graph acquires its HDR format before recording any
|
||||
/// enhanced pass and releases it when the pack retires. The clean retail path
|
||||
/// never acquires a lease and therefore creates no HDR world variants.
|
||||
/// </summary>
|
||||
internal interface IGpuPipelineFormatVariantHost
|
||||
{
|
||||
IDisposable AcquirePipelineColorFormat(GpuTextureFormat format);
|
||||
}
|
||||
|
|
@ -39,6 +39,9 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
/// <summary><c>gl_DrawID</c>. Resets per indirect dispatch exactly as GL's does.</summary>
|
||||
public required bool ShaderDrawParameters { get; init; }
|
||||
|
||||
/// <summary>Optional Vulkan 1.1 core multiview support for layered shadow cascades.</summary>
|
||||
public required bool Multiview { get; init; }
|
||||
|
||||
// ---- 1.2 ----
|
||||
|
||||
/// <summary>One monotonic serial replaces the GL fence array; the retirement ledger keeps its keys.</summary>
|
||||
|
|
@ -93,6 +96,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
TextureCompressionBc = true,
|
||||
SamplerAnisotropy = true,
|
||||
ShaderDrawParameters = true,
|
||||
Multiview = true,
|
||||
TimelineSemaphore = true,
|
||||
HostQueryReset = true,
|
||||
RuntimeDescriptorArray = true,
|
||||
|
|
@ -123,6 +127,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
var n when Is(n, nameof(TextureCompressionBc)) => this with { TextureCompressionBc = false },
|
||||
var n when Is(n, nameof(SamplerAnisotropy)) => this with { SamplerAnisotropy = false },
|
||||
var n when Is(n, nameof(ShaderDrawParameters)) => this with { ShaderDrawParameters = false },
|
||||
var n when Is(n, nameof(Multiview)) => this with { Multiview = false },
|
||||
var n when Is(n, nameof(TimelineSemaphore)) => this with { TimelineSemaphore = false },
|
||||
var n when Is(n, nameof(HostQueryReset)) => this with { HostQueryReset = false },
|
||||
var n when Is(n, nameof(RuntimeDescriptorArray)) => this with { RuntimeDescriptorArray = false },
|
||||
|
|
@ -170,6 +175,15 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
/// </summary>
|
||||
public required uint MaxDescriptorSetStorageBuffersDynamic { get; init; }
|
||||
|
||||
/// <summary>Must reach every storage binding declared by descriptor set 0.</summary>
|
||||
public required uint MaxDescriptorSetStorageBuffers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Must reach every set-0 storage binding because the shared layout exposes
|
||||
/// all of them to both the vertex and fragment stages.
|
||||
/// </summary>
|
||||
public required uint MaxPerStageDescriptorStorageBuffers { get; init; }
|
||||
|
||||
/// <summary>Must reach the number of dynamic uniform bindings set 1 declares.</summary>
|
||||
public required uint MaxDescriptorSetUniformBuffersDynamic { get; init; }
|
||||
|
||||
|
|
@ -185,12 +199,25 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
/// <summary>Ring allocations must satisfy this; getting it wrong is a driver error on Vulkan.</summary>
|
||||
public required uint MinStorageBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Largest legal range in one storage-buffer descriptor. Vulkan 1.3
|
||||
/// guarantees at least 128 MiB; enhanced scene buffers are bounded by the
|
||||
/// actual adapter value rather than a renderer-authored constant.
|
||||
/// </summary>
|
||||
public required uint MaxStorageBufferRange { get; init; }
|
||||
|
||||
/// <summary>As above, for the SceneLighting uniform block.</summary>
|
||||
public required uint MinUniformBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>Largest 2D image edge; the terrain atlas and composite arrays are sized against it.</summary>
|
||||
public required uint MaxImageDimension2D { get; init; }
|
||||
|
||||
/// <summary>Largest image-array layer count reported by the selected physical device.</summary>
|
||||
public required uint MaxImageArrayLayers { get; init; }
|
||||
|
||||
/// <summary>Sum of device-local heap bytes reported by the selected physical device.</summary>
|
||||
public required ulong DeviceLocalHeapBytes { get; init; }
|
||||
|
||||
/// <summary>Highest colour sample count the framebuffer supports, as a plain count (1/2/4/8...).</summary>
|
||||
public required uint MaxColorSampleCount { get; init; }
|
||||
|
||||
|
|
@ -205,16 +232,22 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
MaxPushConstantsSize = GpuBindingModel.MaxPushConstantBytes,
|
||||
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
|
||||
MaxBoundDescriptorSets = 4,
|
||||
// Vulkan's guaranteed minimums. That the layout fits inside them is the
|
||||
// point of slice V6g's split — see VulkanPipelineLayouts.
|
||||
// The dynamic counts use Vulkan's guaranteed minimums. Total/per-stage
|
||||
// counts use acdream's shared-layout requirement, which the startup
|
||||
// capability gate verifies on the real device.
|
||||
MaxDescriptorSetStorageBuffersDynamic = 4,
|
||||
MaxDescriptorSetStorageBuffers = GpuBindingModel.StorageBindingCount,
|
||||
MaxPerStageDescriptorStorageBuffers = GpuBindingModel.StorageBindingCount,
|
||||
MaxDescriptorSetUniformBuffersDynamic = 8,
|
||||
MaxDescriptorSetUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
|
||||
MaxPerStageDescriptorUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
|
||||
TimestampComputeAndGraphics = true,
|
||||
MinStorageBufferOffsetAlignment = 256,
|
||||
MaxStorageBufferRange = 128u * 1024u * 1024u,
|
||||
MinUniformBufferOffsetAlignment = 256,
|
||||
MaxImageDimension2D = 16384,
|
||||
MaxImageArrayLayers = 2048,
|
||||
DeviceLocalHeapBytes = 8UL * 1024 * 1024 * 1024,
|
||||
MaxColorSampleCount = 8,
|
||||
};
|
||||
}
|
||||
|
|
@ -236,6 +269,24 @@ internal sealed record VulkanFormatSupport
|
|||
/// <summary>The chosen depth+stencil format, or <see cref="Format.Undefined"/> when none is usable.</summary>
|
||||
public required Format DepthStencilFormat { get; init; }
|
||||
|
||||
/// <summary>Whether the chosen combined depth/stencil format is sampleable through its depth aspect.</summary>
|
||||
public required bool DepthStencilSampled { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports optimal-tiling colour-attachment writes.</summary>
|
||||
public required bool Rgba16FloatColorAttachment { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports optimal-tiling sampled-image reads.</summary>
|
||||
public required bool Rgba16FloatSampled { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports linear filtering, required by scaled bloom/ray passes.</summary>
|
||||
public required bool Rgba16FloatLinearFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Highest supported RGBA16F sample count for a colour-attachment image.
|
||||
/// Zero means the format/usage combination is unavailable.
|
||||
/// </summary>
|
||||
public required uint MaxRgba16FloatSampleCount { get; init; }
|
||||
|
||||
/// <summary>BC1 (DXT1) sampled-image support with optimal tiling.</summary>
|
||||
public required bool Bc1Sampled { get; init; }
|
||||
|
||||
|
|
@ -249,6 +300,11 @@ internal sealed record VulkanFormatSupport
|
|||
{
|
||||
SwapchainUnormFormat = true,
|
||||
DepthStencilFormat = Format.D32SfloatS8Uint,
|
||||
DepthStencilSampled = true,
|
||||
Rgba16FloatColorAttachment = true,
|
||||
Rgba16FloatSampled = true,
|
||||
Rgba16FloatLinearFilter = true,
|
||||
MaxRgba16FloatSampleCount = 8,
|
||||
Bc1Sampled = true,
|
||||
Bc2Sampled = true,
|
||||
Bc3Sampled = true,
|
||||
|
|
@ -368,23 +424,36 @@ internal sealed record VulkanCapabilityRecord(
|
|||
Math.Min(
|
||||
Limits.MaxDescriptorSetUpdateAfterBindSampledImages,
|
||||
Limits.MaxPerStageDescriptorUpdateAfterBindSampledImages),
|
||||
// Sets 0..2 give each binding its own namespace, so the storage
|
||||
// bindings the model declares (nine, since Campaign V slice V11
|
||||
// deleted the GL-only StorageTextureTable binding) are always all
|
||||
// available once the set count requirement passes. There is no
|
||||
// per-set binding-count limit in Vulkan below
|
||||
// maxPerStageDescriptorStorageBuffers, which is far higher.
|
||||
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
|
||||
MaxStorageBufferBindings = Math.Min(
|
||||
Limits.MaxDescriptorSetStorageBuffers,
|
||||
Limits.MaxPerStageDescriptorStorageBuffers),
|
||||
MaxPushConstantBytes = Limits.MaxPushConstantsSize,
|
||||
MinStorageBufferOffsetAlignment = Limits.MinStorageBufferOffsetAlignment,
|
||||
MaxStorageBufferRangeBytes = Limits.MaxStorageBufferRange,
|
||||
MinUniformBufferOffsetAlignment = Limits.MinUniformBufferOffsetAlignment,
|
||||
MaxClipDistances = Limits.MaxClipDistances,
|
||||
MaxSampleCount = Limits.MaxColorSampleCount,
|
||||
MaxImageDimension2D = Limits.MaxImageDimension2D,
|
||||
MaxImageArrayLayers = Limits.MaxImageArrayLayers,
|
||||
DeviceLocalMemoryBytes = Limits.DeviceLocalHeapBytes,
|
||||
SupportsMultiDrawIndirect = Features.MultiDrawIndirect,
|
||||
SupportsDrawParameters = Features.ShaderDrawParameters,
|
||||
SupportsTextureCompressionBc = Features.TextureCompressionBc,
|
||||
SupportsTimestampQueries = Limits.TimestampComputeAndGraphics,
|
||||
SupportsMultiview = Features.Multiview,
|
||||
SupportsPersistentlyMappedRings = true,
|
||||
SupportsRgba16FloatRenderTargets =
|
||||
Formats.Rgba16FloatColorAttachment
|
||||
&& Formats.Rgba16FloatSampled
|
||||
&& Formats.Rgba16FloatLinearFilter
|
||||
&& Formats.MaxRgba16FloatSampleCount > 0,
|
||||
MaxRgba16FloatSampleCount =
|
||||
Formats.Rgba16FloatColorAttachment
|
||||
&& Formats.Rgba16FloatSampled
|
||||
&& Formats.Rgba16FloatLinearFilter
|
||||
? Math.Min(Formats.MaxRgba16FloatSampleCount, Limits.MaxColorSampleCount)
|
||||
: 0u,
|
||||
SupportsSampledDepth = Formats.DepthStencilSampled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +547,18 @@ internal static class VulkanCapabilityRequirements
|
|||
$"set 0 declares {VulkanPipelineLayouts.DynamicStorageBindingCount} dynamic storage bindings " +
|
||||
$"(Vulkan guarantees 4); this device provides {limits.MaxDescriptorSetStorageBuffersDynamic}.");
|
||||
}
|
||||
if (limits.MaxDescriptorSetStorageBuffers < GpuBindingModel.StorageBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
$"set 0 declares {GpuBindingModel.StorageBindingCount} total storage bindings; " +
|
||||
$"this device provides {limits.MaxDescriptorSetStorageBuffers} per set.");
|
||||
}
|
||||
if (limits.MaxPerStageDescriptorStorageBuffers < GpuBindingModel.StorageBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
$"set 0 exposes {GpuBindingModel.StorageBindingCount} storage bindings to each shader stage; " +
|
||||
$"this device provides {limits.MaxPerStageDescriptorStorageBuffers} per stage.");
|
||||
}
|
||||
if (limits.MaxDescriptorSetUniformBuffersDynamic < VulkanFrameBindings.DynamicUniformBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -68,8 +72,13 @@ internal sealed class VulkanRenderFrameClearPhase : IRenderFrameClearPhase
|
|||
Math.Clamp(atmosphere.FogColor.Z, 0f, 1f),
|
||||
1f);
|
||||
|
||||
var foundation = new RenderFrameFoundation(
|
||||
portalViewportVisible,
|
||||
sky,
|
||||
atmosphere);
|
||||
_clear.ClearColor = clear;
|
||||
return new RenderFrameFoundation(portalViewportVisible, sky, atmosphere);
|
||||
_clear.Foundation = foundation;
|
||||
return foundation;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,19 +117,39 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
private readonly Func<int> _sampleCount;
|
||||
private readonly VulkanWorldPassScope _scope;
|
||||
private readonly IWorldSceneFramePhase _world;
|
||||
private readonly RenderPackController? _renderPacks;
|
||||
private readonly AtmosphericFrameInputState? _atmosphere;
|
||||
private readonly Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
|
||||
_applyRenderPackBoundary;
|
||||
private readonly RenderSceneShadowRuntime? _renderScene;
|
||||
private readonly WbDrawDispatcher? _worldMeshes;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
|
||||
public VulkanWorldScenePhase(
|
||||
ICurrentGpuFrameSource frames,
|
||||
VulkanBackbufferClearState clear,
|
||||
Func<int> sampleCount,
|
||||
VulkanWorldPassScope scope,
|
||||
IWorldSceneFramePhase world)
|
||||
IWorldSceneFramePhase world,
|
||||
RenderPackController? renderPacks = null,
|
||||
AtmosphericFrameInputState? atmosphere = null,
|
||||
Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
|
||||
applyRenderPackBoundary = null,
|
||||
RenderSceneShadowRuntime? renderScene = null,
|
||||
WbDrawDispatcher? worldMeshes = null,
|
||||
TerrainModernRenderer? terrain = null)
|
||||
{
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
_clear = clear ?? throw new ArgumentNullException(nameof(clear));
|
||||
_sampleCount = sampleCount ?? throw new ArgumentNullException(nameof(sampleCount));
|
||||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_renderPacks = renderPacks;
|
||||
_atmosphere = atmosphere;
|
||||
_applyRenderPackBoundary = applyRenderPackBoundary;
|
||||
_renderScene = renderScene;
|
||||
_worldMeshes = worldMeshes;
|
||||
_terrain = terrain;
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||
|
|
@ -129,6 +158,264 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The Vulkan world phase requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
|
||||
|
||||
int samples = _sampleCount();
|
||||
if (_renderPacks is not null)
|
||||
{
|
||||
var extent = new RenderPackActivationExtent(
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples);
|
||||
_ = _applyRenderPackBoundary is not null
|
||||
? _applyRenderPackBoundary(extent)
|
||||
: _renderPacks.ApplyAtFrameBoundary(extent);
|
||||
}
|
||||
if (_renderPacks?.ActiveRuntime is { } active)
|
||||
{
|
||||
if (active is IDefaultWorldPathRenderPackRuntime)
|
||||
return RenderRetail(frame, input);
|
||||
if (active is not IAtmosphericWorldGraphRuntime graph
|
||||
|| _atmosphere is null)
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"The selected pack has no compatible production world graph.");
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
IAtmosphericCpuStageProfileRuntime? cpuStageProfile =
|
||||
graph as IAtmosphericCpuStageProfileRuntime;
|
||||
bool profileCpuStages = cpuStageProfile?.ShouldProfileCpuFrame(frame.Serial) == true;
|
||||
long packCpuTicks = 0;
|
||||
long targetPreparationTicks = 0;
|
||||
IGpuRenderTarget target;
|
||||
long packStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
target = graph.PrepareWorldTarget(
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples);
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric target creation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
long elapsed = Stopwatch.GetTimestamp() - packStarted;
|
||||
packCpuTicks += elapsed;
|
||||
if (profileCpuStages)
|
||||
targetPreparationTicks = elapsed;
|
||||
}
|
||||
|
||||
_atmosphere.BeginFrame(in input, _clear.Foundation);
|
||||
PreparedWorldSceneFrame? prepared = null;
|
||||
if (graph is IDirectionalShadowWorldGraphRuntime directional)
|
||||
{
|
||||
if (_world is not IPreparedWorldSceneFramePhase preparedWorld
|
||||
|| _renderScene is null
|
||||
|| _worldMeshes is null
|
||||
|| _terrain is null)
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"The selected directional-shadow pack has no compatible world preparation seam.");
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
PreparedWorldSceneFrame value;
|
||||
try
|
||||
{
|
||||
value = preparedWorld.PrepareEnhanced(input);
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric world preparation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
prepared = value;
|
||||
if (value.ShouldRender)
|
||||
{
|
||||
packStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
RenderSceneQuery scene = _renderScene.Query;
|
||||
RenderFrameFoundation preparedFoundation = value.Foundation;
|
||||
WorldRenderFrame preparedWorldFrame = value.World;
|
||||
directional.RenderDirectionalShadows(
|
||||
frame,
|
||||
in preparedFoundation,
|
||||
in preparedWorldFrame,
|
||||
value.ActiveDayGroup,
|
||||
in scene,
|
||||
_worldMeshes,
|
||||
_terrain);
|
||||
}
|
||||
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
throw;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
// DirectionalShadowRenderer may have completed its depth
|
||||
// pass and published the pack-owned retained transform
|
||||
// prefix before a later graph check fails (notably the
|
||||
// scene-dependent retained-VRAM ceiling). Cancel the
|
||||
// dispatcher's borrowed same-frame slice before
|
||||
// OnRuntimeFailure disposes the pack and its buffers;
|
||||
// RenderRetail below must allocate its ordinary N.5
|
||||
// transforms from the frame ring, never append to that
|
||||
// retired prefix.
|
||||
_worldMeshes.CancelDirectionalShadowTransformFrame(frame);
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Directional shadow rendering failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
|
||||
}
|
||||
}
|
||||
}
|
||||
WorldRenderFrameOutcome outcome;
|
||||
long receiverCpuTicks = 0;
|
||||
try
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
Name = "atmospheric-world-hdr",
|
||||
Color = new GpuColorAttachment(
|
||||
target,
|
||||
GpuLoadOp.Clear,
|
||||
samples > 1 ? GpuStoreOp.Resolve : GpuStoreOp.Store,
|
||||
_clear.ClearColor),
|
||||
Depth = new GpuDepthAttachment(
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
1f,
|
||||
0),
|
||||
SampleCount = samples,
|
||||
});
|
||||
using IDisposable publication = prepared is { ShouldRender: true }
|
||||
? _scope.PublishPrepared(encoder)
|
||||
: _scope.Publish(encoder);
|
||||
if (prepared is { } value)
|
||||
{
|
||||
using IDisposable receiverTimer = encoder.BeginTimerScope(
|
||||
RenderPackPerformanceScopeNames.EnhancedWorldReceiver);
|
||||
long receiverStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
outcome = ((IPreparedWorldSceneFramePhase)_world)
|
||||
.RenderPreparedEnhanced(input, in value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
receiverCpuTicks += Stopwatch.GetTimestamp() - receiverStarted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outcome = _world.Render(input);
|
||||
}
|
||||
}
|
||||
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
if (prepared is { } value
|
||||
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
}
|
||||
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (prepared is { } value
|
||||
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
}
|
||||
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
|
||||
// The HDR pass may already contain receiver commands, so it
|
||||
// cannot be replayed through retail in this frame. Quarantine
|
||||
// the pack, return an empty outcome for this one aborted frame,
|
||||
// and let the next frame use the unchanged default renderer.
|
||||
_renderPacks?.OnRuntimeFailure(
|
||||
"Atmospheric world rendering failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return default;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AtmosphericFrameInputs atmospheric = _atmosphere.Snapshot();
|
||||
packStarted = Stopwatch.GetTimestamp();
|
||||
graph.RenderPostProcess(frame, in atmospheric);
|
||||
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
|
||||
var observation = new RenderPackFramePerformanceObservation(
|
||||
PackAddedCpuMilliseconds: packCpuTicks * 1000d / Stopwatch.Frequency,
|
||||
StableFrameBoundary: outcome.NormalWorldDrawn,
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples,
|
||||
AbsoluteEnhancedWorldReceiverCpuMilliseconds:
|
||||
receiverCpuTicks * 1000d / Stopwatch.Frequency);
|
||||
bool observationSucceeded = false;
|
||||
long observeStarted = profileCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
try
|
||||
{
|
||||
_renderPacks.ObserveActiveFrame(in observation);
|
||||
observationSucceeded = true;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric performance observation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
}
|
||||
long observeBookkeepingTicks = profileCpuStages
|
||||
? Stopwatch.GetTimestamp() - observeStarted
|
||||
: 0L;
|
||||
if (observationSucceeded && profileCpuStages)
|
||||
{
|
||||
cpuStageProfile!.CompleteCpuProfile(
|
||||
frame.Serial,
|
||||
targetPreparationTicks,
|
||||
packCpuTicks,
|
||||
observeBookkeepingTicks,
|
||||
outcome.NormalWorldDrawn);
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
// The canonical world transaction has already completed and
|
||||
// cannot legally be replayed. Keep its outcome, quarantine the
|
||||
// pack, and let the next frame use the unchanged default path.
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric post-processing failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
private WorldRenderFrameOutcome RenderRetail(
|
||||
IGpuFrame frame,
|
||||
RenderFrameInput input)
|
||||
{
|
||||
// This is the exact pre-pack pass/resource/pipeline path. Keep the branch
|
||||
// whole so Retail selection does not create, touch, or query any pack
|
||||
// object after ApplyAtFrameBoundary reports no active runtime.
|
||||
int samples = _sampleCount();
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
|
|
@ -167,6 +454,8 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
internal sealed class VulkanBackbufferClearState
|
||||
{
|
||||
internal System.Numerics.Vector4 ClearColor { get; set; } = new(0f, 0f, 0f, 1f);
|
||||
|
||||
internal RenderFrameFoundation Foundation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ internal readonly unsafe struct VulkanAllocation(
|
|||
ulong offsetBytes,
|
||||
ulong sizeBytes,
|
||||
uint memoryTypeIndex,
|
||||
MemoryPropertyFlags memoryProperties,
|
||||
VulkanMemoryRange range,
|
||||
void* mapped)
|
||||
{
|
||||
|
|
@ -19,6 +20,7 @@ internal readonly unsafe struct VulkanAllocation(
|
|||
internal ulong OffsetBytes { get; } = offsetBytes;
|
||||
internal ulong SizeBytes { get; } = sizeBytes;
|
||||
internal uint MemoryTypeIndex { get; } = memoryTypeIndex;
|
||||
internal MemoryPropertyFlags MemoryProperties { get; } = memoryProperties;
|
||||
internal VulkanMemoryRange Range { get; } = range;
|
||||
|
||||
/// <summary>First mapped byte of this allocation, or null on device-local memory.</summary>
|
||||
|
|
@ -64,6 +66,7 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
private readonly MemoryPropertyFlags[] _memoryTypeProperties;
|
||||
private readonly ulong _blockSizeBytes;
|
||||
private readonly ulong _dedicatedThresholdBytes;
|
||||
private readonly object _sync = new();
|
||||
|
||||
private readonly Dictionary<uint, VulkanMemoryTypePool> _pools = [];
|
||||
private readonly Dictionary<(uint TypeIndex, int BlockIndex), BlockMemory> _blockMemory = [];
|
||||
|
|
@ -110,9 +113,11 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
GpuMemoryResidency residency,
|
||||
string ownerName)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
uint typeIndex = VulkanMemoryTypeSelection.Choose(
|
||||
uint typeIndex = VulkanMemoryTypeSelection.Choose(
|
||||
_memoryTypeProperties,
|
||||
requirements.MemoryTypeBits,
|
||||
residency)
|
||||
|
|
@ -121,64 +126,69 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
$"Allowed type bits 0x{requirements.MemoryTypeBits:X8}; the device exposes " +
|
||||
$"{_memoryTypeProperties.Length} memory types.");
|
||||
|
||||
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
|
||||
{
|
||||
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
|
||||
_pools.Add(typeIndex, pool);
|
||||
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
|
||||
{
|
||||
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
|
||||
_pools.Add(typeIndex, pool);
|
||||
}
|
||||
|
||||
ulong size = requirements.Size;
|
||||
ulong alignment = Math.Max(requirements.Alignment, 1);
|
||||
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
|
||||
{
|
||||
bool dedicated = pool.IsDedicatedSize(size);
|
||||
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
|
||||
int blockIndex = pool.AddBlock(capacity, dedicated);
|
||||
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
|
||||
|
||||
range = dedicated
|
||||
? pool.AllocateWholeBlock(blockIndex, size)
|
||||
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
|
||||
? placed
|
||||
: throw new InvalidOperationException(
|
||||
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
|
||||
$"allocation at alignment {alignment} for '{ownerName}'.");
|
||||
}
|
||||
|
||||
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
|
||||
AllocatedBytes += range.SizeBytes;
|
||||
void* mapped = block.Mapped == 0
|
||||
? null
|
||||
: (void*)(block.Mapped + (nint)range.OffsetBytes);
|
||||
return new VulkanAllocation(
|
||||
block.Memory,
|
||||
range.OffsetBytes,
|
||||
range.SizeBytes,
|
||||
typeIndex,
|
||||
_memoryTypeProperties[(int)typeIndex],
|
||||
range,
|
||||
mapped);
|
||||
}
|
||||
|
||||
ulong size = requirements.Size;
|
||||
ulong alignment = Math.Max(requirements.Alignment, 1);
|
||||
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
|
||||
{
|
||||
bool dedicated = pool.IsDedicatedSize(size);
|
||||
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
|
||||
int blockIndex = pool.AddBlock(capacity, dedicated);
|
||||
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
|
||||
|
||||
range = dedicated
|
||||
? pool.AllocateWholeBlock(blockIndex, size)
|
||||
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
|
||||
? placed
|
||||
: throw new InvalidOperationException(
|
||||
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
|
||||
$"allocation at alignment {alignment} for '{ownerName}'.");
|
||||
}
|
||||
|
||||
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
|
||||
AllocatedBytes += range.SizeBytes;
|
||||
void* mapped = block.Mapped == 0
|
||||
? null
|
||||
: (void*)(block.Mapped + (nint)range.OffsetBytes);
|
||||
return new VulkanAllocation(
|
||||
block.Memory,
|
||||
range.OffsetBytes,
|
||||
range.SizeBytes,
|
||||
typeIndex,
|
||||
range,
|
||||
mapped);
|
||||
}
|
||||
|
||||
/// <summary>Returns an allocation's bytes to its pool, freeing the block when a dedicated one empties.</summary>
|
||||
internal void Free(in VulkanAllocation allocation)
|
||||
{
|
||||
if (_disposed || allocation.SizeBytes == 0)
|
||||
return;
|
||||
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
|
||||
return;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed || allocation.SizeBytes == 0)
|
||||
return;
|
||||
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
|
||||
return;
|
||||
|
||||
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
|
||||
if (!pool.Free(allocation.Range))
|
||||
return;
|
||||
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
|
||||
if (!pool.Free(allocation.Range))
|
||||
return;
|
||||
|
||||
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
|
||||
if (!_blockMemory.Remove(key, out BlockMemory block))
|
||||
return;
|
||||
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
|
||||
if (!_blockMemory.Remove(key, out BlockMemory block))
|
||||
return;
|
||||
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateBlockMemory(uint typeIndex, int blockIndex, ulong capacityBytes, string ownerName)
|
||||
|
|
@ -216,27 +226,35 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>Human-readable accounting for the diagnostics report and for teardown assertions.</summary>
|
||||
internal string Describe() =>
|
||||
$"{DeviceMemoryObjectCount} device-memory object(s), " +
|
||||
$"{CommittedBytes / (1024 * 1024)} MiB committed, " +
|
||||
$"{AllocatedBytes / (1024 * 1024)} MiB allocated";
|
||||
internal string Describe()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return $"{DeviceMemoryObjectCount} device-memory object(s), "
|
||||
+ $"{CommittedBytes / (1024 * 1024)} MiB committed, "
|
||||
+ $"{AllocatedBytes / (1024 * 1024)} MiB allocated";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (BlockMemory block in _blockMemory.Values)
|
||||
lock (_sync)
|
||||
{
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
}
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
_blockMemory.Clear();
|
||||
_pools.Clear();
|
||||
AllocatedBytes = 0;
|
||||
CommittedBytes = 0;
|
||||
foreach (BlockMemory block in _blockMemory.Values)
|
||||
{
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
}
|
||||
|
||||
_blockMemory.Clear();
|
||||
_pools.Clear();
|
||||
AllocatedBytes = 0;
|
||||
CommittedBytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal file
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
internal readonly record struct VulkanDirectionalMultiviewRange(uint BaseLayer, uint LayerCount);
|
||||
|
||||
internal static class VulkanDirectionalMultiviewContract
|
||||
{
|
||||
internal static VulkanDirectionalMultiviewRange Resolve(uint viewMask, int targetLayerCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetLayerCount);
|
||||
if (targetLayerCount > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(targetLayerCount));
|
||||
uint expected = (1u << targetLayerCount) - 1u;
|
||||
if (viewMask != expected)
|
||||
throw new NotSupportedException("Directional multiview must cover every contiguous target layer.");
|
||||
return new VulkanDirectionalMultiviewRange(0u, (uint)targetLayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One sampleable depth array plus a 2-D attachment view for every cascade.
|
||||
/// Layout is tracked per layer because cascades are produced in distinct
|
||||
/// dynamic-rendering passes and become shader-readable independently.
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanDirectionalDepthTarget : IGpuDirectionalDepthTarget
|
||||
{
|
||||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private readonly ImageView[] _layerViews;
|
||||
private readonly ImageLayout[] _layerLayouts;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanDirectionalDepthTarget(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
VulkanDeviceMemoryAllocator allocator,
|
||||
VulkanUploadQueue uploads,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
VulkanDebugNames debugNames,
|
||||
in GpuDirectionalDepthTargetDescription description,
|
||||
Format depthStencilFormat)
|
||||
{
|
||||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Description = description;
|
||||
|
||||
var textureDescription = new GpuTextureDescription(
|
||||
description.Name,
|
||||
GpuTextureKind.Texture2DArray,
|
||||
description.DepthFormat,
|
||||
description.Resolution,
|
||||
description.Resolution,
|
||||
description.LayerCount,
|
||||
MipLevelCount: 1);
|
||||
Texture = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
textureDescription,
|
||||
sampleCount: 1,
|
||||
renderTarget: true,
|
||||
sampleable: true,
|
||||
formatOverride: depthStencilFormat);
|
||||
|
||||
_layerViews = new ImageView[description.LayerCount];
|
||||
_layerLayouts = new ImageLayout[description.LayerCount];
|
||||
try
|
||||
{
|
||||
for (int layer = 0; layer < _layerViews.Length; layer++)
|
||||
{
|
||||
var create = new ImageViewCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageViewCreateInfo,
|
||||
Image = Texture.Image,
|
||||
ViewType = ImageViewType.Type2D,
|
||||
Format = Texture.VkFormat,
|
||||
SubresourceRange = new ImageSubresourceRange
|
||||
{
|
||||
AspectMask = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
BaseMipLevel = 0,
|
||||
LevelCount = 1,
|
||||
BaseArrayLayer = (uint)layer,
|
||||
LayerCount = 1,
|
||||
},
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateImageView(device, &create, null, out ImageView view),
|
||||
$"vkCreateImageView ('{description.Name}', layer {layer})");
|
||||
_layerViews[layer] = view;
|
||||
debugNames.NameImageView(view, $"{description.Name}-layer-{layer}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (ImageView view in _layerViews)
|
||||
{
|
||||
if (view.Handle != 0)
|
||||
vk.DestroyImageView(device, view, null);
|
||||
}
|
||||
Texture.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public GpuDirectionalDepthTargetDescription Description { get; }
|
||||
|
||||
public IGpuTexture DepthTexture => Texture;
|
||||
|
||||
internal VulkanGpuTexture Texture { get; }
|
||||
|
||||
internal ImageView ViewAt(int layer)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(layer);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, _layerViews.Length);
|
||||
return _layerViews[layer];
|
||||
}
|
||||
|
||||
internal ImageView MultiviewView(uint viewMask)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_ = VulkanDirectionalMultiviewContract.Resolve(viewMask, Description.LayerCount);
|
||||
return Texture.View;
|
||||
}
|
||||
|
||||
internal int LayerCountForViewMask(uint viewMask)
|
||||
{
|
||||
return checked((int)VulkanDirectionalMultiviewContract.Resolve(
|
||||
viewMask,
|
||||
Description.LayerCount).LayerCount);
|
||||
}
|
||||
|
||||
internal ImageLayout LayoutAt(int layer) => _layerLayouts[layer];
|
||||
|
||||
internal void MarkLayout(int layer, ImageLayout layout) => _layerLayouts[layer] = layout;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
ImageView[] views = [.. _layerViews];
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
foreach (ImageView view in views)
|
||||
_vk.DestroyImageView(_device, view, null);
|
||||
});
|
||||
Texture.Dispose();
|
||||
}
|
||||
}
|
||||
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal file
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Per-pass descriptor-bind state. Vulkan descriptor bindings survive pipeline
|
||||
/// changes and remain valid until their layout or dynamic offsets change, so a
|
||||
/// draw can omit an identical second <c>vkCmdBindDescriptorSets</c> command.
|
||||
/// A new pass receives a fresh state and therefore always binds before its
|
||||
/// first draw.
|
||||
/// </summary>
|
||||
internal struct VulkanDrawBindingState
|
||||
{
|
||||
private ulong _pipelineLayout;
|
||||
private int _packGeneration;
|
||||
private bool _hasBinding;
|
||||
private bool _dirty;
|
||||
|
||||
internal readonly bool RequiresBind(
|
||||
ulong pipelineLayout,
|
||||
int packGeneration) =>
|
||||
!_hasBinding
|
||||
|| _dirty
|
||||
|| _pipelineLayout != pipelineLayout
|
||||
|| _packGeneration != packGeneration;
|
||||
|
||||
internal void MarkDirty() => _dirty = true;
|
||||
|
||||
internal void MarkBound(ulong pipelineLayout, int packGeneration)
|
||||
{
|
||||
_pipelineLayout = pipelineLayout;
|
||||
_packGeneration = packGeneration;
|
||||
_hasBinding = true;
|
||||
_dirty = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,10 +26,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
///
|
||||
/// <para><b>Every binding is always bound, whether a renderer uses it or
|
||||
/// not.</b> Bindings a shader does not declare still need a live descriptor, so
|
||||
/// unused ones point at a shared dummy range. That is what lets there be ONE
|
||||
/// descriptor set layout and one pipeline layout rather than a permutation per
|
||||
/// renderer — plan §4.4's requirement, and the thing that makes switching
|
||||
/// pipelines mid-pass free.</para>
|
||||
/// unused ones point at a shared dummy range. Retail keeps its one common
|
||||
/// layout; opt-in render packs add exactly one compatible set rather than
|
||||
/// changing these sets or creating renderer permutations.</para>
|
||||
///
|
||||
/// <para><b>Slice V6i: one set pair per renderer scope.</b> There is no longer a
|
||||
/// single (set 0, set 1) pair per flight slot; there is an arena of them, and
|
||||
|
|
@ -45,8 +44,16 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
private readonly Device _device;
|
||||
private readonly VulkanPipelineLayouts.Created _layouts;
|
||||
private readonly VulkanBindingScopeArena _arena;
|
||||
private readonly uint _maxStorageBufferRangeBytes;
|
||||
private readonly List<DescriptorPool> _pools = [];
|
||||
private readonly List<(DescriptorSet Storage, DescriptorSet Uniform)> _sets = [];
|
||||
private readonly ulong[] _packBuffers = new ulong[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly uint[] _packOffsets = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly uint[] _packRanges = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly Dictionary<PackBindingKey, int> _packSlotsByState = [];
|
||||
private readonly List<DescriptorSet> _packSets = [];
|
||||
private int _packLiveCount;
|
||||
private int _packGeneration = -1;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -76,8 +83,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bindings 0..4 of set 1. Slice V6i-2 raised this from 4 when the layout
|
||||
/// gained binding 4 (sky params); binding 0 remains unused and is counted
|
||||
/// Bindings 0..4 of retail set 1. Binding 0 remains unused and is counted
|
||||
/// only so the bookkeeping arrays stay index-aligned with the binding number.
|
||||
/// Which of them the layout DECLARES is
|
||||
/// <see cref="VulkanPipelineLayouts.IsDeclaredUniformBinding"/>.
|
||||
|
|
@ -87,45 +93,46 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
/// <summary>
|
||||
/// How many of set 1's bindings the layout actually declares, all dynamic.
|
||||
/// Asserted against <c>maxDescriptorSetUniformBuffersDynamic</c> by the
|
||||
/// capability gate; Vulkan guarantees 8, so this is comfortable.
|
||||
/// capability gate; Vulkan guarantees exactly the four bindings declared.
|
||||
/// </summary>
|
||||
internal static uint DynamicUniformBindingCount { get; } =
|
||||
(uint)VulkanPipelineLayouts.DeclaredUniformBindings.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Widest range any single binding may address. Dynamic descriptors take a
|
||||
/// static range at write time and slide it with an offset, so this bounds
|
||||
/// how much of the ring one binding can see at once.
|
||||
/// </summary>
|
||||
internal const uint MaxBindingRangeBytes = 4 * 1024 * 1024;
|
||||
|
||||
internal VulkanFrameBindings(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
VulkanPipelineLayouts.Created layouts,
|
||||
VulkanGpuBuffer ring,
|
||||
VulkanGpuBuffer dummy)
|
||||
VulkanGpuBuffer dummy,
|
||||
uint maxStorageBufferRangeBytes)
|
||||
{
|
||||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
|
||||
ArgumentNullException.ThrowIfNull(ring);
|
||||
ArgumentNullException.ThrowIfNull(dummy);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxStorageBufferRangeBytes, 16u);
|
||||
|
||||
Ring = ring;
|
||||
Dummy = dummy;
|
||||
_maxStorageBufferRangeBytes = maxStorageBufferRangeBytes;
|
||||
_arena = new VulkanBindingScopeArena(
|
||||
(int)GpuBindingModel.StorageBindingCount,
|
||||
UniformBindingCount,
|
||||
VulkanPipelineLayouts.IsDynamicStorageBinding);
|
||||
|
||||
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, MaxBindingRangeBytes);
|
||||
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, _maxStorageBufferRangeBytes);
|
||||
for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++)
|
||||
_arena.SeedStorage(binding, dummy.Handle.Handle, offsetBytes: 0, dummyStorageRange);
|
||||
|
||||
uint dummyUniformRange = (uint)Math.Min(dummy.SizeBytes, 65536);
|
||||
for (uint binding = 0; binding < UniformBindingCount; binding++)
|
||||
_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange);
|
||||
for (int binding = 0; binding < _packBuffers.Length; binding++)
|
||||
{
|
||||
_packBuffers[binding] = dummy.Handle.Handle;
|
||||
_packRanges[binding] = dummyUniformRange;
|
||||
}
|
||||
}
|
||||
|
||||
internal VulkanGpuBuffer Ring { get; }
|
||||
|
|
@ -144,7 +151,12 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
/// previous submission has retired before <c>BeginFrame</c> returns, which is
|
||||
/// the same guarantee that lets the ring rewind.
|
||||
/// </summary>
|
||||
internal void BeginFrame() => _arena.BeginFrame();
|
||||
internal void BeginFrame()
|
||||
{
|
||||
_arena.BeginFrame();
|
||||
_packSlotsByState.Clear();
|
||||
_packLiveCount = 0;
|
||||
}
|
||||
|
||||
internal void SetStorage(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
|
|
@ -159,16 +171,33 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
|
||||
internal void SetUniform(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, (uint)UniformBindingCount);
|
||||
_arena.SetUniform(
|
||||
binding,
|
||||
buffer.Handle.Handle,
|
||||
offsetBytes,
|
||||
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
|
||||
if (binding < UniformBindingCount)
|
||||
{
|
||||
_arena.SetUniform(
|
||||
binding,
|
||||
buffer.Handle.Handle,
|
||||
offsetBytes,
|
||||
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(binding, GpuBindingModel.UniformAtmosphericFrame);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(binding, GpuBindingModel.UniformPackSettings);
|
||||
int packBinding = (int)(binding - GpuBindingModel.UniformAtmosphericFrame);
|
||||
_packBuffers[packBinding] = buffer.Handle.Handle;
|
||||
_packOffsets[packBinding] = offsetBytes;
|
||||
_packRanges[packBinding] = Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536);
|
||||
}
|
||||
|
||||
/// <summary>Binds all three sets with the current dynamic offsets.</summary>
|
||||
internal void Bind(CommandBuffer commands, VulkanGpuDevice device)
|
||||
/// <summary>
|
||||
/// Binds retail sets 0..2. A flagged pipeline additionally supplies its
|
||||
/// live pack state, which lazily materialises and binds set 3.
|
||||
/// </summary>
|
||||
internal void Bind(
|
||||
CommandBuffer commands,
|
||||
VulkanGpuDevice device,
|
||||
PipelineLayout pipelineLayout,
|
||||
VulkanPipelineLayouts.Created.PackState? packState = null)
|
||||
{
|
||||
(int index, int slot, bool needsWrite) = _arena.Resolve();
|
||||
if (slot < 0)
|
||||
|
|
@ -199,12 +228,71 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
_vk.CmdBindDescriptorSets(
|
||||
commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
device.Layouts.PipelineLayout,
|
||||
pipelineLayout,
|
||||
0,
|
||||
3,
|
||||
sets,
|
||||
(uint)dynamicCount,
|
||||
offsets);
|
||||
|
||||
if (packState is not null)
|
||||
BindPackSet(commands, pipelineLayout, packState);
|
||||
}
|
||||
|
||||
private void BindPackSet(
|
||||
CommandBuffer commands,
|
||||
PipelineLayout pipelineLayout,
|
||||
VulkanPipelineLayouts.Created.PackState state)
|
||||
{
|
||||
if (_packGeneration != state.Generation)
|
||||
{
|
||||
_packGeneration = state.Generation;
|
||||
_packSets.Clear();
|
||||
_packSlotsByState.Clear();
|
||||
_packLiveCount = 0;
|
||||
}
|
||||
|
||||
PackBindingKey key = CurrentPackKey();
|
||||
if (!_packSlotsByState.TryGetValue(key, out int slot))
|
||||
{
|
||||
slot = _packLiveCount++;
|
||||
_packSlotsByState.Add(key, slot);
|
||||
if (slot == _packSets.Count)
|
||||
_packSets.Add(state.AllocateDescriptorSet());
|
||||
WritePackSet(_packSets[slot]);
|
||||
}
|
||||
|
||||
DescriptorSet set = _packSets[slot];
|
||||
uint* offsets = stackalloc uint[(int)VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
for (int i = 0; i < _packOffsets.Length; i++)
|
||||
offsets[i] = _packOffsets[i];
|
||||
_vk.CmdBindDescriptorSets(
|
||||
commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
pipelineLayout,
|
||||
GpuBindingModel.RenderPackUniformSet,
|
||||
1,
|
||||
&set,
|
||||
VulkanPipelineLayouts.PackUniformBindingCount,
|
||||
offsets);
|
||||
}
|
||||
|
||||
private PackBindingKey CurrentPackKey() => new(
|
||||
_packBuffers[0], _packRanges[0],
|
||||
_packBuffers[1], _packRanges[1],
|
||||
_packBuffers[2], _packRanges[2],
|
||||
_packBuffers[3], _packRanges[3]);
|
||||
|
||||
private void WritePackSet(DescriptorSet set)
|
||||
{
|
||||
for (int i = 0; i < _packBuffers.Length; i++)
|
||||
{
|
||||
WriteUniform(
|
||||
set,
|
||||
GpuBindingModel.UniformAtmosphericFrame + (uint)i,
|
||||
new Silk.NET.Vulkan.Buffer(_packBuffers[i]),
|
||||
_packRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void WritePair((DescriptorSet Storage, DescriptorSet Uniform) pair)
|
||||
|
|
@ -273,7 +361,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
return pool;
|
||||
}
|
||||
|
||||
private static uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
|
||||
private uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
|
||||
{
|
||||
long remaining = buffer.SizeBytes - offsetBytes;
|
||||
if (remaining <= 0)
|
||||
|
|
@ -285,7 +373,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
"A descriptor range of zero is not representable in Vulkan.");
|
||||
}
|
||||
|
||||
uint available = (uint)Math.Min(remaining, MaxBindingRangeBytes);
|
||||
uint available = (uint)Math.Min(remaining, _maxStorageBufferRangeBytes);
|
||||
return requested == 0 ? available : Math.Min(Math.Max(requested, 16), available);
|
||||
}
|
||||
|
||||
|
|
@ -370,5 +458,17 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
|
||||
_pools.Clear();
|
||||
_sets.Clear();
|
||||
_packSlotsByState.Clear();
|
||||
_packSets.Clear();
|
||||
}
|
||||
|
||||
private readonly record struct PackBindingKey(
|
||||
ulong Buffer0,
|
||||
uint Range0,
|
||||
ulong Buffer1,
|
||||
uint Range1,
|
||||
ulong Buffer2,
|
||||
uint Range2,
|
||||
ulong Buffer3,
|
||||
uint Range3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
|
||||
private readonly IVulkanTimelineApi _timeline;
|
||||
private readonly SortedDictionary<long, List<Action>> _retirements = [];
|
||||
private readonly object _sync = new();
|
||||
|
||||
private long _openSerial;
|
||||
private long _submittedSerial;
|
||||
|
|
@ -99,15 +100,43 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
internal int SlotCount { get; }
|
||||
|
||||
/// <summary>Serial of the frame currently being recorded, or 0 when none is open.</summary>
|
||||
internal long OpenSerial => _openSerial;
|
||||
internal long OpenSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _openSerial;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Highest serial handed to <see cref="EndFrame"/>.</summary>
|
||||
internal long SubmittedSerial => _submittedSerial;
|
||||
internal long SubmittedSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _submittedSerial;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Flight slot index of the currently open frame.</summary>
|
||||
internal int CurrentSlot => SlotIndexOf(_openSerial);
|
||||
internal int CurrentSlot
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return SlotIndexOf(_openSerial);
|
||||
}
|
||||
}
|
||||
|
||||
internal int PendingRetirementCount => _retirements.Sum(entry => entry.Value.Count);
|
||||
internal int PendingRetirementCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _retirements.Sum(entry => entry.Value.Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Maps a frame serial onto its flight slot. Serials are 1-based.</summary>
|
||||
internal int SlotIndexOf(long serial) =>
|
||||
|
|
@ -120,30 +149,36 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
/// </summary>
|
||||
internal long BeginFrame()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_openSerial != 0)
|
||||
lock (_sync)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_openSerial != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
|
||||
}
|
||||
|
||||
long serial = _submittedSerial + 1;
|
||||
long mustComplete = serial - SlotCount;
|
||||
if (mustComplete > 0)
|
||||
_timeline.Wait((ulong)mustComplete);
|
||||
|
||||
_openSerial = serial;
|
||||
RunRetirements();
|
||||
return serial;
|
||||
}
|
||||
|
||||
long serial = _submittedSerial + 1;
|
||||
long mustComplete = serial - SlotCount;
|
||||
if (mustComplete > 0)
|
||||
_timeline.Wait((ulong)mustComplete);
|
||||
|
||||
_openSerial = serial;
|
||||
RunRetirements();
|
||||
return serial;
|
||||
}
|
||||
|
||||
/// <summary>Records that the open frame has been submitted with its serial as the timeline signal value.</summary>
|
||||
internal void EndFrame()
|
||||
{
|
||||
if (_openSerial == 0)
|
||||
return;
|
||||
_submittedSerial = _openSerial;
|
||||
_openSerial = 0;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_openSerial == 0)
|
||||
return;
|
||||
_submittedSerial = _openSerial;
|
||||
_openSerial = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -154,68 +189,83 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
public void Retire(Action release)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
if (_disposed)
|
||||
lock (_sync)
|
||||
{
|
||||
// Teardown already drained the ledger; running immediately is the
|
||||
// only way this release ever happens, and by then the device is idle.
|
||||
release();
|
||||
return;
|
||||
}
|
||||
if (_disposed)
|
||||
{
|
||||
// Teardown already drained the ledger; running immediately is the
|
||||
// only way this release ever happens, and by then the device is idle.
|
||||
release();
|
||||
return;
|
||||
}
|
||||
|
||||
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
|
||||
if (!_retirements.TryGetValue(key, out List<Action>? actions))
|
||||
{
|
||||
actions = [];
|
||||
_retirements.Add(key, actions);
|
||||
}
|
||||
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
|
||||
if (!_retirements.TryGetValue(key, out List<Action>? actions))
|
||||
{
|
||||
actions = [];
|
||||
_retirements.Add(key, actions);
|
||||
}
|
||||
|
||||
actions.Add(release);
|
||||
actions.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs every retirement whose frame the GPU has completed.</summary>
|
||||
internal void RunRetirements()
|
||||
{
|
||||
if (_retirements.Count == 0)
|
||||
return;
|
||||
|
||||
var completed = (long)_timeline.CurrentValue;
|
||||
while (_retirements.Count > 0)
|
||||
lock (_sync)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
if (first.Key > completed)
|
||||
break;
|
||||
if (_retirements.Count == 0)
|
||||
return;
|
||||
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
var completed = (long)_timeline.CurrentValue;
|
||||
while (_retirements.Count > 0)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
if (first.Key > completed)
|
||||
break;
|
||||
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Blocks until every submitted frame has completed, then drains the whole ledger.</summary>
|
||||
internal void WaitForSubmittedWork()
|
||||
{
|
||||
if (_submittedSerial > 0)
|
||||
_timeline.Wait((ulong)_submittedSerial);
|
||||
DrainAll();
|
||||
lock (_sync)
|
||||
{
|
||||
if (_submittedSerial > 0)
|
||||
_timeline.Wait((ulong)_submittedSerial);
|
||||
DrainAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs every pending retirement regardless of serial. Only legal when the device is idle.</summary>
|
||||
internal void DrainAll()
|
||||
{
|
||||
while (_retirements.Count > 0)
|
||||
lock (_sync)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
while (_retirements.Count > 0)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
DrainAll();
|
||||
_disposed = true;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
DrainAll();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ internal sealed unsafe class VulkanGpuBuffer : IGpuBuffer
|
|||
public long SizeBytes { get; }
|
||||
public GpuBufferUsage Usage { get; }
|
||||
public GpuMemoryResidency Residency { get; }
|
||||
public bool HostWritesAreCoherent =>
|
||||
_allocation.MemoryProperties.HasFlag(MemoryPropertyFlags.HostCoherentBit);
|
||||
|
||||
internal Buffer Handle { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
private readonly Dictionary<GpuSamplerDescription, VulkanGpuSampler> _samplers = [];
|
||||
private readonly Dictionary<string, (ShaderModule Vertex, ShaderModule Fragment)> _shaderModules = [];
|
||||
private readonly HashSet<VulkanGpuPipeline> _pipelines = [];
|
||||
private readonly Dictionary<GpuTextureFormat, int> _pipelineFormatLeaseCounts = [];
|
||||
private readonly object _resourceCreationSync = new();
|
||||
private string _shaderSpirvDirectory = string.Empty;
|
||||
private float _maxSamplerAnisotropy = 1f;
|
||||
|
||||
|
|
@ -127,7 +130,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_device,
|
||||
_layouts,
|
||||
_ringBuffers[slot],
|
||||
_bindingDummy);
|
||||
_bindingDummy,
|
||||
Capabilities.MaxStorageBufferRangeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +232,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
}
|
||||
|
||||
_shaderModules.Clear();
|
||||
_pipelines.Clear();
|
||||
_pipelineFormatLeaseCounts.Clear();
|
||||
|
||||
foreach (VulkanGpuSampler sampler in _samplers.Values)
|
||||
sampler.Dispose();
|
||||
|
|
@ -290,6 +296,18 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
public IGpuTexture CreateTexture(in GpuTextureDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (description.Format == GpuTextureFormat.Rgba16FloatRenderTarget
|
||||
&& !Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
|
||||
}
|
||||
if (description.Format == GpuTextureFormat.Depth24Stencil8
|
||||
&& !Capabilities.SupportsSampledDepth)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
|
||||
}
|
||||
return new VulkanGpuTexture(
|
||||
_vk,
|
||||
_device,
|
||||
|
|
@ -303,9 +321,16 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
}
|
||||
|
||||
public IGpuSampler CreateSampler(in GpuSamplerDescription description)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return CreateSamplerLocked(in description);
|
||||
}
|
||||
|
||||
private IGpuSampler CreateSamplerLocked(in GpuSamplerDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing))
|
||||
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing)
|
||||
&& !existing.IsDisposed)
|
||||
return existing;
|
||||
|
||||
var created = new VulkanGpuSampler(
|
||||
|
|
@ -315,13 +340,39 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_debugNames,
|
||||
description,
|
||||
_maxSamplerAnisotropy);
|
||||
_samplers.Add(description, created);
|
||||
_samplers[description] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
|
||||
if ((uint)description.SampleCount > Capabilities.MaxSampleCount)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"The device supports at most {Capabilities.MaxSampleCount} colour/depth samples; "
|
||||
+ $"'{description.Name}' requested {description.SampleCount}.");
|
||||
}
|
||||
if (description.ColorFormat == GpuTextureFormat.Rgba16FloatRenderTarget)
|
||||
{
|
||||
if (!Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are required by this render target.");
|
||||
}
|
||||
if ((uint)description.SampleCount > Capabilities.MaxRgba16FloatSampleCount)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"RGBA16F supports at most {Capabilities.MaxRgba16FloatSampleCount} samples on this device; "
|
||||
+ $"'{description.Name}' requested {description.SampleCount}.");
|
||||
}
|
||||
}
|
||||
if (description.SampleableDepth && !Capabilities.SupportsSampledDepth)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
|
||||
}
|
||||
return new VulkanGpuRenderTarget(
|
||||
_vk,
|
||||
_device,
|
||||
|
|
@ -333,6 +384,39 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
DepthStencilFormat);
|
||||
}
|
||||
|
||||
public IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
|
||||
in GpuDirectionalDepthTargetDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Resolution);
|
||||
if (description.LayerCount is < 2 or > 4)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(description),
|
||||
description.LayerCount,
|
||||
"Directional depth targets require 2-4 cascade layers.");
|
||||
}
|
||||
if (description.DepthFormat != GpuTextureFormat.Depth24Stencil8)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Directional depth targets currently require Depth24Stencil8.",
|
||||
nameof(description));
|
||||
}
|
||||
if (!Capabilities.SupportsSampledDepth)
|
||||
throw new NotSupportedException("Sampled depth is unavailable on this device.");
|
||||
|
||||
return new VulkanDirectionalDepthTarget(
|
||||
_vk,
|
||||
_device,
|
||||
_allocator,
|
||||
_uploads,
|
||||
_flights,
|
||||
_debugNames,
|
||||
description,
|
||||
DepthStencilFormat);
|
||||
}
|
||||
|
||||
public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
|
@ -342,6 +426,12 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
throw new ArgumentException("The Vulkan backend can only register a Vulkan texture.", nameof(texture));
|
||||
if (sampler is not VulkanGpuSampler vulkanSampler)
|
||||
throw new ArgumentException("The Vulkan backend can only register a Vulkan sampler.", nameof(sampler));
|
||||
if (!vulkanTexture.IsSampleable || vulkanTexture.SampledView.Handle == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Texture '{vulkanTexture.Name}' is an attachment-only image and has no sampled view.",
|
||||
nameof(texture));
|
||||
}
|
||||
|
||||
// Campaign V slice V6k made this a loud refusal, and V6l is the slice
|
||||
// that serves it. A render-target image is viewed as
|
||||
|
|
@ -352,7 +442,10 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
// layered view over the same image for exactly this, and every texture
|
||||
// that is not an attachment has always had one; SampledView is that view
|
||||
// in both cases, so the question disappears rather than being answered.
|
||||
return TextureTable.Register(vulkanTexture.SampledView, vulkanSampler.Handle);
|
||||
return TextureTable.Register(
|
||||
vulkanTexture.SampledView,
|
||||
vulkanSampler.Handle,
|
||||
vulkanTexture.SampledLayout);
|
||||
}
|
||||
|
||||
public void ReleaseTextureSlot(GpuTextureSlot slot)
|
||||
|
|
@ -375,11 +468,20 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
/// frame ever pays a shader compile or a driver state revalidation.
|
||||
/// </summary>
|
||||
public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return CreatePipelineLocked(description);
|
||||
}
|
||||
|
||||
private IGpuPipeline CreatePipelineLocked(GpuPipelineDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
if (description.ViewMask != 0 && !Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected Vulkan device does not support multiview pipelines.");
|
||||
|
||||
(ShaderModule vertex, ShaderModule fragment) = LoadShaderModules(description.Shaders.Name);
|
||||
(ShaderModule vertex, ShaderModule fragment, bool ownsModules) =
|
||||
LoadShaderModules(description.Shaders);
|
||||
// Slice V6d: the pipeline names the format it renders into, rather than
|
||||
// every pipeline being hard-coded to one. Rgba8UnormRenderTarget — the
|
||||
// default — still maps to the swapchain's format; see
|
||||
|
|
@ -387,29 +489,164 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
// offscreen targets adopt the swapchain's format rather than the other
|
||||
// way round.
|
||||
Format colorFormat = VulkanTextureFormatMapping.FormatOf(description.ColorFormat);
|
||||
return new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_flights,
|
||||
_debugNames,
|
||||
Layouts.PipelineLayout,
|
||||
_pipelineCache?.Handle ?? default,
|
||||
vertex,
|
||||
fragment,
|
||||
description,
|
||||
colorFormat,
|
||||
DepthStencilFormat);
|
||||
VulkanGpuPipeline pipeline;
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = null;
|
||||
try
|
||||
{
|
||||
packLease = description.UsesRenderPackShaderAbi
|
||||
? Layouts.AcquirePackLayout()
|
||||
: null;
|
||||
pipeline = new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_flights,
|
||||
_debugNames,
|
||||
Layouts,
|
||||
packLease,
|
||||
packLease?.PipelineLayout ?? Layouts.PipelineLayout,
|
||||
_pipelineCache?.Handle ?? default,
|
||||
vertex,
|
||||
fragment,
|
||||
ownsModules,
|
||||
description,
|
||||
colorFormat,
|
||||
DepthStencilFormat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
packLease?.Dispose();
|
||||
if (ownsModules)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, fragment, null);
|
||||
_vk.DestroyShaderModule(_device, vertex, null);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (GpuTextureFormat format in _pipelineFormatLeaseCounts.Keys)
|
||||
pipeline.AddColorFormatVariant(format);
|
||||
_pipelines.Add(pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
catch
|
||||
{
|
||||
pipeline.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private (ShaderModule Vertex, ShaderModule Fragment) LoadShaderModules(string name)
|
||||
public IDisposable AcquirePipelineColorFormat(GpuTextureFormat format)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return AcquirePipelineColorFormatLocked(format);
|
||||
}
|
||||
|
||||
private IDisposable AcquirePipelineColorFormatLocked(GpuTextureFormat format)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (!VulkanTextureFormatMapping.IsRenderTarget(format)
|
||||
|| VulkanTextureFormatMapping.IsDepthStencil(format))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{format} is not a colour render-target format.",
|
||||
nameof(format));
|
||||
}
|
||||
if (format == GpuTextureFormat.Rgba16FloatRenderTarget
|
||||
&& !Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
|
||||
}
|
||||
|
||||
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
|
||||
if (!_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
|
||||
{
|
||||
var added = new List<VulkanGpuPipeline>(_pipelines.Count);
|
||||
try
|
||||
{
|
||||
foreach (VulkanGpuPipeline pipeline in _pipelines)
|
||||
{
|
||||
if (pipeline.AddColorFormatVariant(format))
|
||||
added.Add(pipeline);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (VulkanGpuPipeline pipeline in added)
|
||||
pipeline.RemoveColorFormatVariant(format);
|
||||
throw;
|
||||
}
|
||||
_pipelineFormatLeaseCounts.Add(format, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pipelineFormatLeaseCounts[format] = checked(count + 1);
|
||||
}
|
||||
|
||||
return new PipelineColorFormatLease(this, format);
|
||||
}
|
||||
|
||||
private void ReleasePipelineColorFormat(GpuTextureFormat format)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
{
|
||||
if (_disposed || !_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
|
||||
return;
|
||||
if (count > 1)
|
||||
{
|
||||
_pipelineFormatLeaseCounts[format] = count - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
_pipelineFormatLeaseCounts.Remove(format);
|
||||
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
|
||||
foreach (VulkanGpuPipeline pipeline in _pipelines)
|
||||
pipeline.RemoveColorFormatVariant(format);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PipelineColorFormatLease(
|
||||
VulkanGpuDevice device,
|
||||
GpuTextureFormat format) : IDisposable
|
||||
{
|
||||
private VulkanGpuDevice? _device = device;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _device, null)?.ReleasePipelineColorFormat(format);
|
||||
}
|
||||
|
||||
private (ShaderModule Vertex, ShaderModule Fragment, bool OwnsModules) LoadShaderModules(
|
||||
in GpuShaderSet shaders)
|
||||
{
|
||||
if (shaders.HasEmbeddedSpirv)
|
||||
{
|
||||
ShaderModule embeddedVertex = CreateShaderModule(
|
||||
shaders.Name,
|
||||
"vert",
|
||||
shaders.VertexSpirv.Span);
|
||||
try
|
||||
{
|
||||
return (
|
||||
embeddedVertex,
|
||||
CreateShaderModule(shaders.Name, "frag", shaders.FragmentSpirv.Span),
|
||||
true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, embeddedVertex, null);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
string name = shaders.Name;
|
||||
if (_shaderModules.TryGetValue(name, out (ShaderModule Vertex, ShaderModule Fragment) existing))
|
||||
return existing;
|
||||
return (existing.Vertex, existing.Fragment, false);
|
||||
|
||||
ShaderModule vertex = CreateShaderModule(name, "vert");
|
||||
ShaderModule fragment = CreateShaderModule(name, "frag");
|
||||
_shaderModules[name] = (vertex, fragment);
|
||||
return (vertex, fragment);
|
||||
return (vertex, fragment, false);
|
||||
}
|
||||
|
||||
private ShaderModule CreateShaderModule(string name, string stage)
|
||||
|
|
@ -424,9 +661,19 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
path);
|
||||
}
|
||||
|
||||
byte[] code = File.ReadAllBytes(path);
|
||||
if (code.Length % 4 != 0)
|
||||
throw new InvalidDataException($"'{path}' is {code.Length} bytes, which is not a whole number of SPIR-V words.");
|
||||
return CreateShaderModule(name, stage, File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
private ShaderModule CreateShaderModule(
|
||||
string name,
|
||||
string stage,
|
||||
ReadOnlySpan<byte> code)
|
||||
{
|
||||
if (code.Length < 4 || code.Length % 4 != 0)
|
||||
throw new InvalidDataException(
|
||||
$"'{name}.{stage}' is {code.Length} bytes, which is not valid word-aligned SPIR-V.");
|
||||
if (System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code) != 0x07230203u)
|
||||
throw new InvalidDataException($"'{name}.{stage}' has no SPIR-V header.");
|
||||
|
||||
fixed (byte* first = code)
|
||||
{
|
||||
|
|
@ -505,12 +752,50 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
uint width;
|
||||
uint height;
|
||||
ImageView colorView;
|
||||
ImageView colorView = default;
|
||||
ImageView resolveView = default;
|
||||
ImageView depthView = default;
|
||||
bool backbuffer = description.Color.Target is null;
|
||||
ImageView depthResolveView = default;
|
||||
bool hasColorAttachment = description.HasColorAttachment;
|
||||
bool backbuffer = hasColorAttachment && description.Color.Target is null;
|
||||
uint viewMask = description.ViewMask;
|
||||
GpuTextureFormat passColorFormat = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||||
|
||||
if (backbuffer)
|
||||
if (!hasColorAttachment)
|
||||
{
|
||||
if (description.SampleCount != 1)
|
||||
throw new InvalidOperationException("Directional depth passes are single-sampled.");
|
||||
if (description.Depth is not { DirectionalTarget: VulkanDirectionalDepthTarget target } depth)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A colour-less pass requires a Vulkan directional-depth target.",
|
||||
nameof(description));
|
||||
}
|
||||
if (depth.Store != GpuStoreOp.Store)
|
||||
throw new InvalidOperationException("Directional depth must be stored for later sampling.");
|
||||
if (depth.Layer < 0 || depth.Layer >= target.Description.LayerCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(description), "Directional depth layer is outside the target.");
|
||||
|
||||
width = (uint)target.Description.Resolution;
|
||||
height = (uint)target.Description.Resolution;
|
||||
if (viewMask != 0)
|
||||
{
|
||||
if (!Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected Vulkan device does not support multiview.");
|
||||
depthView = target.MultiviewView(viewMask);
|
||||
TransitionDirectionalDepthForRendering(
|
||||
commands,
|
||||
target,
|
||||
baseLayer: 0,
|
||||
layerCount: target.LayerCountForViewMask(viewMask));
|
||||
}
|
||||
else
|
||||
{
|
||||
depthView = target.ViewAt(depth.Layer);
|
||||
TransitionDirectionalDepthForRendering(commands, target, depth.Layer, 1);
|
||||
}
|
||||
}
|
||||
else if (backbuffer)
|
||||
{
|
||||
if (_backbuffer is null || _acquiredImageIndex is not { } imageIndex)
|
||||
{
|
||||
|
|
@ -545,52 +830,106 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
{
|
||||
if (description.Color.Target is not VulkanGpuRenderTarget target)
|
||||
throw new ArgumentException("The Vulkan backend can only render into a Vulkan render target.");
|
||||
if (target.Description.SampleCount != description.SampleCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pass '{description.Name}' declares {description.SampleCount} samples but target "
|
||||
+ $"'{target.Description.Name}' was created for {target.Description.SampleCount}.");
|
||||
}
|
||||
width = (uint)target.Description.Width;
|
||||
height = (uint)target.Description.Height;
|
||||
colorView = target.Color.View;
|
||||
passColorFormat = target.Description.ColorFormat;
|
||||
colorView = target.ColorAttachment.View;
|
||||
if (target.ColorResolve is { } colorResolve)
|
||||
{
|
||||
if (description.Color.Load == GpuLoadOp.Load)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled target '{target.Description.Name}' cannot Load a prior resolved image; "
|
||||
+ "its transient multisample attachment has no preserved contents.");
|
||||
}
|
||||
if (description.Color.Store != GpuStoreOp.Resolve)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled target '{target.Description.Name}' must use Store=Resolve so its "
|
||||
+ "single-sampled ColorTexture receives this pass.");
|
||||
}
|
||||
resolveView = colorResolve.View;
|
||||
}
|
||||
else if (description.Color.Store == GpuStoreOp.Resolve)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Single-sampled target '{target.Description.Name}' cannot use Store=Resolve.");
|
||||
}
|
||||
TransitionRenderTargetForRendering(commands, target);
|
||||
if (description.Depth is not null && target.Depth is { } depth)
|
||||
if (description.Depth is not null && target.DepthAttachment is { } depth)
|
||||
{
|
||||
depthView = depth.View;
|
||||
if (target.Description.SampleCount > 1
|
||||
&& description.Depth.Value.Load == GpuLoadOp.Load)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled depth target '{target.Description.Name}' cannot Load transient depth.");
|
||||
}
|
||||
if (target.Description.SampleableDepth
|
||||
&& description.Depth.Value.Store != GpuStoreOp.Store)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Sampleable depth on '{target.Description.Name}' requires Store=Store.");
|
||||
}
|
||||
if (target.DepthResolve is { } depthResolve)
|
||||
{
|
||||
depthResolveView = depthResolve.View;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector4 clear = description.Color.ClearColor;
|
||||
var colorAttachment = new RenderingAttachmentInfo
|
||||
RenderingAttachmentInfo colorAttachment = default;
|
||||
if (hasColorAttachment)
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = colorView,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
|
||||
StoreOp = description.Color.Store == GpuStoreOp.Resolve
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(description.Color.Store),
|
||||
ClearValue = new ClearValue
|
||||
Vector4 clear = description.Color.ClearColor;
|
||||
colorAttachment = new RenderingAttachmentInfo
|
||||
{
|
||||
Color = new ClearColorValue
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = colorView,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
|
||||
StoreOp = description.Color.Store == GpuStoreOp.Resolve
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(description.Color.Store),
|
||||
ClearValue = new ClearValue
|
||||
{
|
||||
Float32_0 = clear.X,
|
||||
Float32_1 = clear.Y,
|
||||
Float32_2 = clear.Z,
|
||||
Float32_3 = clear.W,
|
||||
Color = new ClearColorValue
|
||||
{
|
||||
Float32_0 = clear.X,
|
||||
Float32_1 = clear.Y,
|
||||
Float32_2 = clear.Z,
|
||||
Float32_3 = clear.W,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (resolveView.Handle != 0)
|
||||
{
|
||||
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
|
||||
colorAttachment.ResolveImageView = resolveView;
|
||||
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
|
||||
};
|
||||
if (resolveView.Handle != 0)
|
||||
{
|
||||
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
|
||||
colorAttachment.ResolveImageView = resolveView;
|
||||
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
|
||||
}
|
||||
}
|
||||
|
||||
RenderingAttachmentInfo depthAttachment = default;
|
||||
RenderingAttachmentInfo stencilAttachment = default;
|
||||
if (description.Depth is { } depthDescription && depthView.Handle != 0)
|
||||
{
|
||||
bool resolveDepth = depthResolveView.Handle != 0;
|
||||
depthAttachment = new RenderingAttachmentInfo
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = depthView,
|
||||
ImageLayout = ImageLayout.DepthStencilAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(depthDescription.Load),
|
||||
StoreOp = VulkanViewportMapping.ToVulkan(depthDescription.Store),
|
||||
StoreOp = resolveDepth
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(depthDescription.Store),
|
||||
ClearValue = new ClearValue
|
||||
{
|
||||
DepthStencil = new ClearDepthStencilValue(
|
||||
|
|
@ -598,6 +937,19 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
depthDescription.ClearStencil),
|
||||
},
|
||||
};
|
||||
stencilAttachment = depthAttachment;
|
||||
if (resolveDepth)
|
||||
{
|
||||
// SAMPLE_ZERO is guaranteed for both depth and stencil by the
|
||||
// Vulkan 1.3 depth/stencil-resolve contract. Resolving both
|
||||
// aspects avoids depending on independentResolveNone.
|
||||
depthAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
|
||||
depthAttachment.ResolveImageView = depthResolveView;
|
||||
depthAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
stencilAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
|
||||
stencilAttachment.ResolveImageView = depthResolveView;
|
||||
stencilAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
}
|
||||
}
|
||||
|
||||
var rendering = new RenderingInfo
|
||||
|
|
@ -605,13 +957,14 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
SType = StructureType.RenderingInfo,
|
||||
RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D(width, height)),
|
||||
LayerCount = 1,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &colorAttachment,
|
||||
ViewMask = viewMask,
|
||||
ColorAttachmentCount = hasColorAttachment ? 1u : 0u,
|
||||
PColorAttachments = hasColorAttachment ? &colorAttachment : null,
|
||||
PDepthAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &depthAttachment
|
||||
: null,
|
||||
PStencilAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &depthAttachment
|
||||
PStencilAttachment = stencilAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &stencilAttachment
|
||||
: null,
|
||||
};
|
||||
_vk.CmdBeginRendering(commands, &rendering);
|
||||
|
|
@ -625,7 +978,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
description,
|
||||
width,
|
||||
height,
|
||||
hasDepthAttachment: depthView.Handle != 0);
|
||||
hasDepthAttachment: depthView.Handle != 0,
|
||||
hasColorAttachment,
|
||||
colorFormat: passColorFormat);
|
||||
_openPass = encoder;
|
||||
return encoder;
|
||||
}
|
||||
|
|
@ -640,7 +995,29 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_debugNames.EndLabel(commands);
|
||||
|
||||
if (!_openPassIsBackbuffer && encoder.Pass.Color.Target is VulkanGpuRenderTarget target)
|
||||
TransitionRenderTargetForSampling(commands, target);
|
||||
{
|
||||
TransitionRenderTargetForSampling(
|
||||
commands,
|
||||
target,
|
||||
colorStored: encoder.Pass.Color.Store != GpuStoreOp.DontCare,
|
||||
depthStored: encoder.Pass.Depth?.Store == GpuStoreOp.Store);
|
||||
}
|
||||
else if (encoder.Pass.Depth is
|
||||
{ DirectionalTarget: VulkanDirectionalDepthTarget directionalTarget } depth)
|
||||
{
|
||||
if (encoder.Pass.ViewMask != 0)
|
||||
{
|
||||
TransitionDirectionalDepthForSampling(
|
||||
commands,
|
||||
directionalTarget,
|
||||
0,
|
||||
directionalTarget.LayerCountForViewMask(encoder.Pass.ViewMask));
|
||||
}
|
||||
else
|
||||
{
|
||||
TransitionDirectionalDepthForSampling(commands, directionalTarget, depth.Layer, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_openPass = null;
|
||||
}
|
||||
|
|
@ -766,19 +1143,35 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
private void TransitionRenderTargetForRendering(CommandBuffer commands, VulkanGpuRenderTarget target)
|
||||
{
|
||||
VulkanGpuTexture colorAttachment = target.ColorAttachment;
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Color.Image,
|
||||
colorAttachment.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
target.Color.CurrentLayout,
|
||||
colorAttachment.CurrentLayout,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit);
|
||||
target.Color.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
colorAttachment.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
|
||||
if (target.Depth is { } depth)
|
||||
if (target.ColorResolve is { } colorResolve)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
colorResolve.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
colorResolve.CurrentLayout,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit);
|
||||
colorResolve.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
}
|
||||
|
||||
if (target.DepthAttachment is { } depth)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
|
|
@ -788,25 +1181,157 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit);
|
||||
depth.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
|
||||
if (target.DepthResolve is { } depthResolve)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
depthResolve.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
depthResolve.CurrentLayout,
|
||||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit);
|
||||
depthResolve.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionRenderTargetForSampling(CommandBuffer commands, VulkanGpuRenderTarget target)
|
||||
/// <summary>
|
||||
/// Makes retained mapped-storage writes visible to vertex-shader SSBO
|
||||
/// reads. The buffer belongs to the current flight slot, whose prior use has
|
||||
/// retired before the host write; this barrier supplies the in-submission
|
||||
/// HOST_WRITE to SHADER_READ dependency before the shadow pass consumes it.
|
||||
/// </summary>
|
||||
internal void PublishHostStorageWrites(
|
||||
VulkanGpuFrame frame,
|
||||
IGpuBuffer buffer)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
if (!ReferenceEquals(_openFrame, frame))
|
||||
throw new InvalidOperationException("Host writes require the open Vulkan frame.");
|
||||
if (_openPass is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Retained host writes must be published before opening a rendering pass.");
|
||||
}
|
||||
if (buffer is not VulkanGpuBuffer vkBuffer
|
||||
|| buffer.Residency != GpuMemoryResidency.HostWritable
|
||||
|| !buffer.Usage.HasFlag(GpuBufferUsage.Storage))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Published host writes require a Vulkan host-writable storage buffer.",
|
||||
nameof(buffer));
|
||||
}
|
||||
|
||||
CommandBuffer commands = _commandBuffers[frame.SlotIndex];
|
||||
BufferMemoryBarrier2 barrier = VulkanHostStorageVisibility.Create(
|
||||
vkBuffer.Handle,
|
||||
checked((ulong)vkBuffer.SizeBytes));
|
||||
var dependency = new DependencyInfo
|
||||
{
|
||||
SType = StructureType.DependencyInfo,
|
||||
BufferMemoryBarrierCount = 1,
|
||||
PBufferMemoryBarriers = &barrier,
|
||||
};
|
||||
_vk.CmdPipelineBarrier2(commands, &dependency);
|
||||
}
|
||||
|
||||
private void TransitionRenderTargetForSampling(
|
||||
CommandBuffer commands,
|
||||
VulkanGpuRenderTarget target,
|
||||
bool colorStored,
|
||||
bool depthStored)
|
||||
{
|
||||
if (colorStored)
|
||||
{
|
||||
VulkanGpuTexture color = target.ColorResult;
|
||||
TransitionImage(
|
||||
commands,
|
||||
color.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
color.CurrentLayout,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
|
||||
}
|
||||
|
||||
if (depthStored && target.Description.SampleableDepth && target.DepthResult is { } depth)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
depth.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
depth.CurrentLayout,
|
||||
ImageLayout.DepthStencilReadOnlyOptimal,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
depth.MarkLayout(ImageLayout.DepthStencilReadOnlyOptimal);
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionDirectionalDepthForRendering(
|
||||
CommandBuffer commands,
|
||||
VulkanDirectionalDepthTarget target,
|
||||
int baseLayer,
|
||||
int layerCount)
|
||||
{
|
||||
const PipelineStageFlags2 DepthStages =
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit;
|
||||
ImageLayout oldLayout = target.LayoutAt(baseLayer);
|
||||
for (int i = 1; i < layerCount; i++)
|
||||
{
|
||||
if (target.LayoutAt(baseLayer + i) != oldLayout)
|
||||
throw new InvalidOperationException("Multiview directional layers must share one layout.");
|
||||
}
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Texture.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
oldLayout,
|
||||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
oldLayout == ImageLayout.Undefined ? PipelineStageFlags2.TopOfPipeBit : PipelineStageFlags2.FragmentShaderBit,
|
||||
oldLayout == ImageLayout.Undefined ? AccessFlags2.None : AccessFlags2.ShaderReadBit,
|
||||
DepthStages,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
baseArrayLayer: (uint)baseLayer,
|
||||
layerCount: (uint)layerCount);
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
|
||||
private void TransitionDirectionalDepthForSampling(
|
||||
CommandBuffer commands,
|
||||
VulkanDirectionalDepthTarget target,
|
||||
int baseLayer,
|
||||
int layerCount)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Color.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit,
|
||||
target.Texture.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
target.LayoutAt(baseLayer),
|
||||
ImageLayout.DepthStencilReadOnlyOptimal,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
target.Color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
|
||||
AccessFlags2.ShaderReadBit,
|
||||
baseArrayLayer: (uint)baseLayer,
|
||||
layerCount: (uint)layerCount);
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilReadOnlyOptimal);
|
||||
}
|
||||
|
||||
private void TransitionImage(
|
||||
|
|
@ -818,7 +1343,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
PipelineStageFlags2 sourceStage,
|
||||
AccessFlags2 sourceAccess,
|
||||
PipelineStageFlags2 destinationStage,
|
||||
AccessFlags2 destinationAccess)
|
||||
AccessFlags2 destinationAccess,
|
||||
uint baseArrayLayer = 0,
|
||||
uint layerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers)
|
||||
{
|
||||
var barrier = new ImageMemoryBarrier2
|
||||
{
|
||||
|
|
@ -837,8 +1364,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
AspectMask = aspect,
|
||||
BaseMipLevel = 0,
|
||||
LevelCount = Silk.NET.Vulkan.Vk.RemainingMipLevels,
|
||||
BaseArrayLayer = 0,
|
||||
LayerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers,
|
||||
BaseArrayLayer = baseArrayLayer,
|
||||
LayerCount = layerCount,
|
||||
},
|
||||
};
|
||||
var dependency = new DependencyInfo
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ internal interface IVulkanBackbuffer
|
|||
/// signalling both the per-image render-complete semaphore and the timeline at
|
||||
/// this frame's serial, present.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
|
||||
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineFormatVariantHost
|
||||
{
|
||||
/// <summary>Per-flight-slot ring capacity, matching the GL backend's 16 MiB.</summary>
|
||||
internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
|
||||
|
|
@ -150,17 +150,34 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
|
|||
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
|
||||
MaxPushConstantBytes = limits.MaxPushConstantsSize,
|
||||
MinStorageBufferOffsetAlignment = Math.Max(limits.MinStorageBufferOffsetAlignment, 1),
|
||||
MaxStorageBufferRangeBytes = limits.MaxStorageBufferRange,
|
||||
MinUniformBufferOffsetAlignment = Math.Max(limits.MinUniformBufferOffsetAlignment, 1),
|
||||
MaxClipDistances = limits.MaxClipDistances,
|
||||
MaxSampleCount = limits.MaxColorSampleCount,
|
||||
MaxImageDimension2D = limits.MaxImageDimension2D,
|
||||
MaxImageArrayLayers = limits.MaxImageArrayLayers,
|
||||
DeviceLocalMemoryBytes = limits.DeviceLocalHeapBytes,
|
||||
SupportsMultiDrawIndirect = features.MultiDrawIndirect,
|
||||
SupportsDrawParameters = features.ShaderDrawParameters,
|
||||
SupportsTextureCompressionBc =
|
||||
features.TextureCompressionBc && formats.Bc1Sampled && formats.Bc2Sampled && formats.Bc3Sampled,
|
||||
SupportsTimestampQueries = limits.TimestampComputeAndGraphics,
|
||||
SupportsMultiview = features.Multiview,
|
||||
// The one capability that is true here and false on GL, and the
|
||||
// mechanism behind the campaign's CPU-cost target.
|
||||
SupportsPersistentlyMappedRings = true,
|
||||
SupportsRgba16FloatRenderTargets =
|
||||
formats.Rgba16FloatColorAttachment
|
||||
&& formats.Rgba16FloatSampled
|
||||
&& formats.Rgba16FloatLinearFilter
|
||||
&& formats.MaxRgba16FloatSampleCount > 0,
|
||||
MaxRgba16FloatSampleCount =
|
||||
formats.Rgba16FloatColorAttachment
|
||||
&& formats.Rgba16FloatSampled
|
||||
&& formats.Rgba16FloatLinearFilter
|
||||
? Math.Min(formats.MaxRgba16FloatSampleCount, limits.MaxColorSampleCount)
|
||||
: 0u,
|
||||
SupportsSampledDepth = formats.DepthStencilSampled,
|
||||
};
|
||||
|
||||
var timelineType = new SemaphoreTypeCreateInfo
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ internal sealed class VulkanGpuFrame : IGpuFrame
|
|||
public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) =>
|
||||
_device.AllocateRing(SlotIndex, byteCount, usage);
|
||||
|
||||
public void PublishHostStorageWrites(IGpuBuffer buffer) =>
|
||||
_device.PublishHostStorageWrites(this, buffer);
|
||||
|
||||
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// <para><b>Storage and uniform bindings go through a dynamic descriptor
|
||||
/// set.</b> The contract lets a renderer bind an arbitrary buffer range per
|
||||
/// draw, and ring allocations mean that range moves every frame. Rather than
|
||||
/// writing descriptors mid-frame, set 0 and set 1 are allocated per flight slot
|
||||
/// writing descriptors mid-frame, retail sets 0 and 1 are allocated per flight slot
|
||||
/// with DYNAMIC descriptor types and the per-draw offset is supplied at bind
|
||||
/// time — which is what keeps the campaign's "zero descriptor writes per frame"
|
||||
/// property true for buffers as well as for textures.</para>
|
||||
/// time. Opt-in set 3 uses the same rule from a separately owned lazy pool.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
||||
{
|
||||
|
|
@ -31,6 +30,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
private readonly uint _attachmentWidth;
|
||||
private readonly uint _attachmentHeight;
|
||||
private readonly bool _hasDepthAttachment;
|
||||
private readonly bool _hasColorAttachment;
|
||||
private readonly GpuTextureFormat _colorFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Extent of the attachments <c>vkCmdBeginRendering</c> was handed. Campaign V
|
||||
|
|
@ -47,6 +48,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
internal bool HasDepthAttachment => _hasDepthAttachment;
|
||||
|
||||
private VulkanGpuPipeline? _pipeline;
|
||||
private VulkanDrawBindingState _drawBindingState;
|
||||
private bool _closed;
|
||||
|
||||
internal VulkanGpuPassEncoder(
|
||||
|
|
@ -57,7 +59,9 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
GpuPassDescription pass,
|
||||
uint attachmentWidth,
|
||||
uint attachmentHeight,
|
||||
bool hasDepthAttachment)
|
||||
bool hasDepthAttachment,
|
||||
bool hasColorAttachment,
|
||||
GpuTextureFormat colorFormat)
|
||||
{
|
||||
_device = device;
|
||||
_frame = frame;
|
||||
|
|
@ -70,6 +74,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
// the attachments exist gets none, and the pipeline variant has to agree
|
||||
// with the command buffer rather than with the intent.
|
||||
_hasDepthAttachment = hasDepthAttachment;
|
||||
_hasColorAttachment = hasColorAttachment;
|
||||
_colorFormat = colorFormat;
|
||||
Pass = pass;
|
||||
|
||||
// A pass always starts with the whole attachment drawable. GL's
|
||||
|
|
@ -80,23 +86,15 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
SetViewport(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
SetScissor(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
|
||||
// Campaign V slice V6h: and for the same reason, the descriptor sets.
|
||||
// Campaign V slice V6h requires every pass to be self-contained rather
|
||||
// than inheriting descriptor state from an earlier renderer. The first
|
||||
// draw now establishes that state through FlushBindings. Deferring it
|
||||
// until a draw exists avoids recording an unused initial binding and
|
||||
// lets later draws reuse an identical binding safely.
|
||||
//
|
||||
// Before this, sets 0/1/2 were bound only as a side effect of
|
||||
// BindStorageBuffer/BindUniformBuffer, so a pass whose pipeline reads the
|
||||
// texture table but binds no buffer — every retained-UI and debug-line
|
||||
// pass, because their per-draw data travels in push constants and a
|
||||
// vertex buffer — issued vkCmdDraw with set 2 unbound. That is
|
||||
// VUID-vkCmdDraw-None-08600 and, on the RX 9070 XT, an immediate
|
||||
// ErrorDeviceLost at submit.
|
||||
//
|
||||
// It went unseen through V6c–V6g because the bring-up host always drew
|
||||
// VulkanRhiScene first: its storage binds left all three sets bound in
|
||||
// the same command buffer, so the UI pass that followed inherited them.
|
||||
// The composition host has no 3-D scene, so its UI pass is the first
|
||||
// thing in the buffer and inherits nothing. Binding here makes a pass
|
||||
// self-contained rather than dependent on what preceded it in the frame.
|
||||
_bindings.Bind(_commands, _device);
|
||||
// FlushBindings is called by every draw verb, including passes such as
|
||||
// retained UI and debug lines that bind no buffers themselves. Thus set
|
||||
// 2 is still guaranteed before vkCmdDraw and VUID 08600 stays closed.
|
||||
}
|
||||
|
||||
public GpuPassDescription Pass { get; }
|
||||
|
|
@ -107,17 +105,27 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
ThrowIfClosed();
|
||||
if (pipeline is not VulkanGpuPipeline vulkanPipeline)
|
||||
throw new ArgumentException("The Vulkan backend can only bind a Vulkan pipeline.", nameof(pipeline));
|
||||
if (vulkanPipeline.Description.HasColorAttachment != _hasColorAttachment)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{vulkanPipeline.Description.Name}' colour-attachment intent does not match pass '{Pass.Name}'.");
|
||||
}
|
||||
if (vulkanPipeline.Description.ViewMask != Pass.ViewMask)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{vulkanPipeline.Description.Name}' view mask does not match pass '{Pass.Name}'.");
|
||||
}
|
||||
|
||||
_pipeline = vulkanPipeline;
|
||||
_device.Api.CmdBindPipeline(
|
||||
_commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
vulkanPipeline.HandleFor(_hasDepthAttachment));
|
||||
vulkanPipeline.HandleFor(_hasDepthAttachment, _colorFormat));
|
||||
|
||||
// Every pipeline shares one layout, so the descriptor sets and push
|
||||
// constants bound earlier in the pass survive this call. That is the
|
||||
// whole reason for the shared layout, and it is why a bucketed world
|
||||
// pass can change pipeline per bucket for free.
|
||||
// Retail and pack pipelines share sets 0..2 and the same 96-byte push
|
||||
// range, but a pack pipeline has one additional set. Bind against the
|
||||
// exact layout used to create the active pipeline so set 3 can never
|
||||
// leak onto the authoritative retail path.
|
||||
_device.CmdBindPipelineDefaults(_commands, vulkanPipeline.Description);
|
||||
}
|
||||
|
||||
|
|
@ -125,12 +133,14 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
{
|
||||
ThrowIfClosed();
|
||||
_bindings.SetStorage(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
|
||||
_drawBindingState.MarkDirty();
|
||||
}
|
||||
|
||||
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
_bindings.SetUniform(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
|
||||
_drawBindingState.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -149,10 +159,23 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
/// what the arena was designed to produce.</para>
|
||||
///
|
||||
/// <para>Legal because descriptor-set binding is independent of pipeline
|
||||
/// binding when the layouts are compatible, and acdream has ONE pipeline
|
||||
/// layout by design (§4.4).</para>
|
||||
/// binding when the layouts are compatible. Retail and pack layouts share
|
||||
/// identical sets 0..2; the active pipeline supplies the optional set 3.</para>
|
||||
/// </summary>
|
||||
private void FlushBindings() => _bindings.Bind(_commands, _device);
|
||||
private void FlushBindings()
|
||||
{
|
||||
VulkanGpuPipeline pipeline = RequirePipeline();
|
||||
ulong pipelineLayout = pipeline.PipelineLayout.Handle;
|
||||
int packGeneration = pipeline.PackState?.Generation ?? 0;
|
||||
if (!_drawBindingState.RequiresBind(pipelineLayout, packGeneration))
|
||||
return;
|
||||
_bindings.Bind(
|
||||
_commands,
|
||||
_device,
|
||||
pipeline.PipelineLayout,
|
||||
pipeline.PackState);
|
||||
_drawBindingState.MarkBound(pipelineLayout, packGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A scoped clear inside the live render-pass instance — retail's interior
|
||||
|
|
@ -194,7 +217,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
{
|
||||
_device.Api.CmdPushConstants(
|
||||
_commands,
|
||||
_device.Layouts.PipelineLayout,
|
||||
_pipeline?.PipelineLayout ?? _device.Layouts.PipelineLayout,
|
||||
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
0,
|
||||
(uint)GpuBindingModel.PushConstantBytes,
|
||||
|
|
@ -313,10 +336,11 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
return vulkanBuffer;
|
||||
}
|
||||
|
||||
private void RequirePipeline()
|
||||
private VulkanGpuPipeline RequirePipeline()
|
||||
{
|
||||
if (_pipeline is null)
|
||||
throw new InvalidOperationException("BindPipeline must be called before drawing.");
|
||||
return _pipeline;
|
||||
}
|
||||
|
||||
private void ThrowIfClosed() => ObjectDisposedException.ThrowIf(_closed, this);
|
||||
|
|
|
|||
|
|
@ -43,8 +43,18 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private readonly VulkanDebugNames _debugNames;
|
||||
private readonly VulkanPipelineLayouts.Created _layouts;
|
||||
private readonly VulkanPipelineLayouts.Created.PackLayoutLease? _packLayoutLease;
|
||||
private readonly PipelineLayout _layout;
|
||||
private readonly PipelineCache _cache;
|
||||
private readonly ShaderModule _vertexModule;
|
||||
private readonly ShaderModule _fragmentModule;
|
||||
private readonly bool _ownsShaderModules;
|
||||
private readonly Format _depthStencilFormat;
|
||||
private readonly Pipeline _withDepthAttachment;
|
||||
private readonly Pipeline _withoutDepthAttachment;
|
||||
private readonly Dictionary<GpuTextureFormat, VulkanGpuPipeline> _colorVariants = [];
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanGpuPipeline(
|
||||
|
|
@ -52,10 +62,13 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
Device device,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
VulkanDebugNames debugNames,
|
||||
VulkanPipelineLayouts.Created layouts,
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLayoutLease,
|
||||
PipelineLayout layout,
|
||||
PipelineCache cache,
|
||||
ShaderModule vertexModule,
|
||||
ShaderModule fragmentModule,
|
||||
bool ownsShaderModules,
|
||||
GpuPipelineDescription description,
|
||||
Format colorFormat,
|
||||
Format depthStencilFormat)
|
||||
|
|
@ -63,6 +76,15 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
_debugNames = debugNames ?? throw new ArgumentNullException(nameof(debugNames));
|
||||
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
|
||||
_packLayoutLease = packLayoutLease;
|
||||
_layout = layout;
|
||||
_cache = cache;
|
||||
_vertexModule = vertexModule;
|
||||
_fragmentModule = fragmentModule;
|
||||
_ownsShaderModules = ownsShaderModules;
|
||||
_depthStencilFormat = depthStencilFormat;
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
|
||||
nint entryPoint = SilkMarshal.StringToPtr("main");
|
||||
|
|
@ -211,8 +233,8 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
{
|
||||
SType = StructureType.PipelineColorBlendStateCreateInfo,
|
||||
LogicOpEnable = false,
|
||||
AttachmentCount = 1,
|
||||
PAttachments = &attachment,
|
||||
AttachmentCount = description.HasColorAttachment ? 1u : 0u,
|
||||
PAttachments = description.HasColorAttachment ? &attachment : null,
|
||||
};
|
||||
|
||||
DynamicState* dynamicStates = stackalloc DynamicState[9];
|
||||
|
|
@ -246,8 +268,9 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
var rendering = new PipelineRenderingCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineRenderingCreateInfo,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachmentFormats = &color,
|
||||
ViewMask = description.ViewMask,
|
||||
ColorAttachmentCount = description.HasColorAttachment ? 1u : 0u,
|
||||
PColorAttachmentFormats = description.HasColorAttachment ? &color : null,
|
||||
DepthAttachmentFormat = depthStencilFormat,
|
||||
StencilAttachmentFormat = depthStencilFormat,
|
||||
};
|
||||
|
|
@ -312,17 +335,107 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
internal Pipeline HandleFor(bool passHasDepthAttachment) =>
|
||||
passHasDepthAttachment ? _withDepthAttachment : _withoutDepthAttachment;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the prebuilt attachment-format variant required by the live pass.
|
||||
/// Missing variants fail before a draw can record undefined Vulkan usage.
|
||||
/// </summary>
|
||||
internal Pipeline HandleFor(
|
||||
bool passHasDepthAttachment,
|
||||
GpuTextureFormat colorFormat)
|
||||
{
|
||||
if (colorFormat == Description.ColorFormat)
|
||||
return HandleFor(passHasDepthAttachment);
|
||||
if (_colorVariants.TryGetValue(colorFormat, out VulkanGpuPipeline? variant))
|
||||
return variant.HandleFor(passHasDepthAttachment);
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{Description.Name}' has no prebuilt {colorFormat} attachment variant.");
|
||||
}
|
||||
|
||||
internal bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>The exact layout this pipeline was created against.</summary>
|
||||
internal PipelineLayout PipelineLayout => _layout;
|
||||
|
||||
/// <summary>Non-null only for a pipeline flagged for render-pack ABI v1.</summary>
|
||||
internal VulkanPipelineLayouts.Created.PackState? PackState => _packLayoutLease?.State;
|
||||
|
||||
internal bool AddColorFormatVariant(GpuTextureFormat format)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!Description.HasColorAttachment
|
||||
|| !Description.AllowColorFormatVariants
|
||||
|| format == Description.ColorFormat
|
||||
|| _colorVariants.ContainsKey(format))
|
||||
return false;
|
||||
|
||||
var variantDescription = Description with
|
||||
{
|
||||
Name = $"{Description.Name}-{format.ToString().ToLowerInvariant()}",
|
||||
ColorFormat = format,
|
||||
AllowColorFormatVariants = false,
|
||||
};
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease =
|
||||
Description.UsesRenderPackShaderAbi ? _layouts.AcquirePackLayout() : null;
|
||||
VulkanGpuPipeline variant;
|
||||
try
|
||||
{
|
||||
variant = new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_retirement,
|
||||
_debugNames,
|
||||
_layouts,
|
||||
packLease,
|
||||
packLease?.PipelineLayout ?? _layouts.PipelineLayout,
|
||||
_cache,
|
||||
_vertexModule,
|
||||
_fragmentModule,
|
||||
ownsShaderModules: false,
|
||||
variantDescription,
|
||||
VulkanTextureFormatMapping.FormatOf(format),
|
||||
_depthStencilFormat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
packLease?.Dispose();
|
||||
throw;
|
||||
}
|
||||
_colorVariants.Add(format, variant);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void RemoveColorFormatVariant(GpuTextureFormat format)
|
||||
{
|
||||
if (_colorVariants.Remove(format, out VulkanGpuPipeline? variant))
|
||||
variant.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
foreach (VulkanGpuPipeline variant in _colorVariants.Values)
|
||||
variant.Dispose();
|
||||
_colorVariants.Clear();
|
||||
Pipeline withDepth = _withDepthAttachment;
|
||||
Pipeline withoutDepth = _withoutDepthAttachment;
|
||||
ShaderModule vertex = _vertexModule;
|
||||
ShaderModule fragment = _fragmentModule;
|
||||
bool destroyModules = _ownsShaderModules;
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = _packLayoutLease;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
_vk.DestroyPipeline(_device, withDepth, null);
|
||||
_vk.DestroyPipeline(_device, withoutDepth, null);
|
||||
if (destroyModules)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, fragment, null);
|
||||
_vk.DestroyShaderModule(_device, vertex, null);
|
||||
}
|
||||
// The optional set-3 and four-set layout cannot be destroyed until
|
||||
// every pipeline that names them has actually retired.
|
||||
packLease?.Dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// offscreen colour(+depth) bundle behind the paperdoll, the creature-appraisal
|
||||
/// viewport and the portal mask.
|
||||
///
|
||||
/// <para>Offscreen targets stay single-sampled, matching the contract. Their
|
||||
/// colour image carries <c>SAMPLED</c> as well as <c>COLOR_ATTACHMENT</c> usage
|
||||
/// so it can be registered into the texture table and drawn by the retained UI
|
||||
/// the moment its pass ends — which is the whole reason these exist rather than
|
||||
/// rendering those views onto the backbuffer.</para>
|
||||
/// <para>The textures exposed through <see cref="IGpuRenderTarget"/> are always
|
||||
/// single-sampled. When the requested attachment sample count is greater than
|
||||
/// one, separate transient multisample attachments resolve into those textures;
|
||||
/// the global table never receives an illegal multisampled view.</para>
|
||||
///
|
||||
/// <para>Slice V6l made both halves of that sentence true. The colour image now
|
||||
/// carries a second, LAYERED view for the table to sample (see
|
||||
|
|
@ -22,7 +21,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
||||
{
|
||||
private readonly VulkanGpuTexture _color;
|
||||
private readonly VulkanGpuTexture? _multisampleColor;
|
||||
private readonly VulkanGpuTexture? _depth;
|
||||
private readonly VulkanGpuTexture? _multisampleDepth;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanGpuRenderTarget(
|
||||
|
|
@ -38,8 +39,18 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Width);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
|
||||
if (description.SampleableDepth && description.DepthFormat is null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"SampleableDepth requires a depth format.",
|
||||
nameof(description));
|
||||
}
|
||||
Description = description;
|
||||
|
||||
// The public colour texture is the single-sampled result even when the
|
||||
// pass itself is multisampled. Post-process and retained-UI consumers
|
||||
// always register this image, never the transient attachment below.
|
||||
_color = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
|
|
@ -55,11 +66,36 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
Math.Max(1, description.SampleCount),
|
||||
renderTarget: true);
|
||||
sampleCount: 1,
|
||||
renderTarget: true,
|
||||
sampleable: true);
|
||||
|
||||
if (description.SampleCount > 1)
|
||||
{
|
||||
_multisampleColor = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
new GpuTextureDescription(
|
||||
$"{description.Name}-color-msaa",
|
||||
GpuTextureKind.Texture2D,
|
||||
description.ColorFormat,
|
||||
description.Width,
|
||||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
description.SampleCount,
|
||||
renderTarget: true,
|
||||
sampleable: false);
|
||||
}
|
||||
|
||||
if (description.DepthFormat is { } depthFormat)
|
||||
{
|
||||
int retainedDepthSamples =
|
||||
description.SampleableDepth ? 1 : description.SampleCount;
|
||||
_depth = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
|
|
@ -75,14 +111,38 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
Math.Max(1, description.SampleCount),
|
||||
retainedDepthSamples,
|
||||
renderTarget: true,
|
||||
sampleable: description.SampleableDepth,
|
||||
// Slice V6l: the DEVICE's combined depth/stencil format, not the
|
||||
// contract enum's literal one. Every pipeline bakes one
|
||||
// depth/stencil format under dynamic rendering and the same
|
||||
// pipelines draw in both the backbuffer pass and this one, so a
|
||||
// second format here would make one of the two undefined.
|
||||
formatOverride: deviceDepthStencilFormat);
|
||||
|
||||
if (description.SampleableDepth && description.SampleCount > 1)
|
||||
{
|
||||
_multisampleDepth = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
new GpuTextureDescription(
|
||||
$"{description.Name}-depth-msaa",
|
||||
GpuTextureKind.Texture2D,
|
||||
depthFormat,
|
||||
description.Width,
|
||||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
description.SampleCount,
|
||||
renderTarget: true,
|
||||
sampleable: false,
|
||||
formatOverride: deviceDepthStencilFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +150,33 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
|
||||
public IGpuTexture ColorTexture => _color;
|
||||
|
||||
internal VulkanGpuTexture Color => _color;
|
||||
public IGpuTexture? DepthTexture => Description.SampleableDepth ? _depth : null;
|
||||
|
||||
internal VulkanGpuTexture? Depth => _depth;
|
||||
/// <summary>The image written as the pass's colour attachment.</summary>
|
||||
internal VulkanGpuTexture ColorAttachment => _multisampleColor ?? _color;
|
||||
|
||||
/// <summary>The single-sampled resolve destination, or null at one sample.</summary>
|
||||
internal VulkanGpuTexture? ColorResolve => _multisampleColor is null ? null : _color;
|
||||
|
||||
/// <summary>The image written as the pass's depth/stencil attachment.</summary>
|
||||
internal VulkanGpuTexture? DepthAttachment => _multisampleDepth ?? _depth;
|
||||
|
||||
/// <summary>The sampleable depth resolve destination, or null when no resolve is required.</summary>
|
||||
internal VulkanGpuTexture? DepthResolve =>
|
||||
Description.SampleableDepth && _multisampleDepth is not null ? _depth : null;
|
||||
|
||||
internal VulkanGpuTexture ColorResult => _color;
|
||||
|
||||
internal VulkanGpuTexture? DepthResult => _depth;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_multisampleDepth?.Dispose();
|
||||
_depth?.Dispose();
|
||||
_multisampleColor?.Dispose();
|
||||
_color.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
in GpuTextureDescription description,
|
||||
int sampleCount = 1,
|
||||
bool renderTarget = false,
|
||||
bool sampleable = true,
|
||||
// Fully qualified: in a parameter-default expression the simple name
|
||||
// `Format` binds to this type's own GpuTextureFormat property first.
|
||||
Format formatOverride = Silk.NET.Vulkan.Format.Undefined)
|
||||
|
|
@ -55,6 +56,13 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.LayerCount);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.MipLevelCount);
|
||||
if (sampleCount > 1 && sampleable)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A multisampled image cannot be registered in acdream's single-sampled texture table; "
|
||||
+ "create a separate single-sampled resolve image.",
|
||||
nameof(sampleable));
|
||||
}
|
||||
|
||||
Name = description.Name;
|
||||
Kind = description.Kind;
|
||||
|
|
@ -64,6 +72,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
LayerCount = description.LayerCount;
|
||||
MipLevelCount = description.MipLevelCount;
|
||||
SampleCount = sampleCount;
|
||||
IsSampleable = sampleable;
|
||||
// Slice V6l: an offscreen target's DEPTH attachment takes the format the
|
||||
// device already chose for the backbuffer, because a pipeline bakes one
|
||||
// depth/stencil format and draws in both kinds of pass. The contract's
|
||||
|
|
@ -77,7 +86,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
bool depthStencil = VulkanTextureFormatMapping.IsDepthStencil(description.Format);
|
||||
ImageUsageFlags usage = depthStencil
|
||||
? ImageUsageFlags.DepthStencilAttachmentBit
|
||||
: ImageUsageFlags.SampledBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
|
||||
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
|
||||
if (sampleable)
|
||||
usage |= ImageUsageFlags.SampledBit;
|
||||
if (renderTarget && !depthStencil)
|
||||
usage |= ImageUsageFlags.ColorAttachmentBit;
|
||||
if (sampleCount > 1)
|
||||
|
|
@ -142,9 +153,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
_vk.CreateImageView(_device, &viewCreate, null, out ImageView view),
|
||||
$"vkCreateImageView ('{description.Name}')");
|
||||
View = view;
|
||||
SampledView = view;
|
||||
SampledView = renderTarget && !sampleable ? default : view;
|
||||
|
||||
// Campaign V slice V6l: a colour render target needs TWO views.
|
||||
// Campaign V slice V6l: a sampleable render target needs TWO views.
|
||||
//
|
||||
// An ATTACHMENT view must be VK_IMAGE_VIEW_TYPE_2D, and the global
|
||||
// texture table's descriptor array is declared sampler2DArray, so the
|
||||
|
|
@ -155,9 +166,14 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
// fix: one image, one allocation, two ways of looking at it. Legal
|
||||
// without any creation flag — a 2D_ARRAY view over an imageType-2D
|
||||
// image with arrayLayers >= 1 is exactly what the spec permits.
|
||||
if (renderTarget && !depthStencil)
|
||||
if (renderTarget && sampleable)
|
||||
{
|
||||
viewCreate.ViewType = VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind);
|
||||
// Combined depth/stencil remains one attachment for #117, but
|
||||
// sampling exposes only depth. A sampled view containing the
|
||||
// stencil aspect is invalid for sampler2DArray.
|
||||
if (depthStencil)
|
||||
viewCreate.SubresourceRange.AspectMask = ImageAspectFlags.DepthBit;
|
||||
VulkanInterop.Check(
|
||||
_vk.CreateImageView(_device, &viewCreate, null, out ImageView sampled),
|
||||
$"vkCreateImageView ('{description.Name}', sampled)");
|
||||
|
|
@ -184,6 +200,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
public int MipLevelCount { get; }
|
||||
|
||||
internal int SampleCount { get; }
|
||||
internal bool IsSampleable { get; }
|
||||
internal Image Image { get; }
|
||||
|
||||
/// <summary>The view a pass names as an attachment, and the only view a non-attachment has.</summary>
|
||||
|
|
@ -196,6 +213,10 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
/// is <c>sampler2DArray</c> (slice V6l, plan §5.5.7).
|
||||
/// </summary>
|
||||
internal ImageView SampledView { get; }
|
||||
internal ImageLayout SampledLayout =>
|
||||
VulkanTextureFormatMapping.IsDepthStencil(Format)
|
||||
? ImageLayout.DepthStencilReadOnlyOptimal
|
||||
: ImageLayout.ShaderReadOnlyOptimal;
|
||||
internal Format VkFormat { get; }
|
||||
internal ImageAspectFlags Aspect { get; }
|
||||
|
||||
|
|
@ -264,7 +285,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
VulkanAllocation allocation = _allocation;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
if (sampledView.Handle != view.Handle)
|
||||
if (sampledView.Handle != 0 && sampledView.Handle != view.Handle)
|
||||
_vk.DestroyImageView(_device, sampledView, null);
|
||||
_vk.DestroyImageView(_device, view, null);
|
||||
_vk.DestroyImage(_device, image, null);
|
||||
|
|
@ -337,6 +358,8 @@ internal sealed unsafe class VulkanGpuSampler : IGpuSampler
|
|||
|
||||
internal Sampler Handle { get; }
|
||||
|
||||
internal bool IsDisposed => _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ internal sealed unsafe class VulkanGpuTimerPool : IGpuTimerPool, IDisposable
|
|||
/// <see cref="TryResolve"/> deliberately reports the last known value
|
||||
/// forever — right for a diagnostic readout, wrong for a percentile.
|
||||
/// </summary>
|
||||
internal bool TryTakeResolved(string scopeName, out double milliseconds)
|
||||
public bool TryTakeResolved(string scopeName, out double milliseconds)
|
||||
{
|
||||
if (!_resolved.TryGetValue(scopeName, out milliseconds))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -214,6 +214,13 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
|
||||
_physicalDevice = handles[choice.Device.Index];
|
||||
|
||||
// The logical-device feature chain consumes this exact probe result.
|
||||
// Keep the probe owned by the selected physical device and publish it
|
||||
// before VulkanLogicalDeviceFactory.Create: probing later leaves the
|
||||
// production Acquire path with no safe value from which to decide
|
||||
// whether the optional Vulkan 1.1 multiview feature may be enabled.
|
||||
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
|
||||
|
||||
IReadOnlyList<VulkanQueueFamilyCandidate> queueFamilies =
|
||||
VulkanPhysicalDeviceInspector.ReadQueueFamilies(
|
||||
vk,
|
||||
|
|
@ -233,7 +240,8 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
vk,
|
||||
_physicalDevice,
|
||||
families,
|
||||
requireSwapchain: true);
|
||||
requireSwapchain: true,
|
||||
availableFeatures: _features);
|
||||
_device = created.Device;
|
||||
_graphicsQueue = created.GraphicsQueue;
|
||||
_presentQueue = created.PresentQueue;
|
||||
|
|
@ -287,7 +295,6 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
_graphicsQueue,
|
||||
families.GraphicsFamily);
|
||||
|
||||
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
|
||||
_limits = VulkanPhysicalDeviceInspector.ReadLimits(vk, _physicalDevice);
|
||||
_formats = VulkanPhysicalDeviceInspector.ReadFormats(
|
||||
vk,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using Silk.NET.Vulkan;
|
||||
using Buffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Exact sync2 dependency for CPU writes into a retained mapped SSBO before
|
||||
/// shadow vertex shaders read it. Kept pure so driverless contract tests can
|
||||
/// assert the stage/access/ownership/range tuple Vulkan receives.
|
||||
/// </summary>
|
||||
internal static class VulkanHostStorageVisibility
|
||||
{
|
||||
internal static BufferMemoryBarrier2 Create(Buffer buffer, ulong sizeBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfZero(sizeBytes);
|
||||
return new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
SrcStageMask = PipelineStageFlags2.HostBit,
|
||||
SrcAccessMask = AccessFlags2.HostWriteBit,
|
||||
DstStageMask = PipelineStageFlags2.VertexShaderBit,
|
||||
DstAccessMask = AccessFlags2.ShaderReadBit,
|
||||
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
||||
Buffer = buffer,
|
||||
Offset = 0,
|
||||
Size = sizeBytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -314,6 +314,7 @@ internal static unsafe class VulkanPhysicalDeviceInspector
|
|||
TextureCompressionBc = core.TextureCompressionBC,
|
||||
SamplerAnisotropy = core.SamplerAnisotropy,
|
||||
ShaderDrawParameters = vulkan11.ShaderDrawParameters,
|
||||
Multiview = vulkan11.Multiview,
|
||||
TimelineSemaphore = vulkan12.TimelineSemaphore,
|
||||
HostQueryReset = vulkan12.HostQueryReset,
|
||||
RuntimeDescriptorArray = vulkan12.RuntimeDescriptorArray,
|
||||
|
|
@ -363,6 +364,8 @@ internal static unsafe class VulkanPhysicalDeviceInspector
|
|||
MaxClipDistances = limits.MaxClipDistances,
|
||||
MaxBoundDescriptorSets = limits.MaxBoundDescriptorSets,
|
||||
MaxDescriptorSetStorageBuffersDynamic = limits.MaxDescriptorSetStorageBuffersDynamic,
|
||||
MaxDescriptorSetStorageBuffers = limits.MaxDescriptorSetStorageBuffers,
|
||||
MaxPerStageDescriptorStorageBuffers = limits.MaxPerStageDescriptorStorageBuffers,
|
||||
MaxDescriptorSetUniformBuffersDynamic = limits.MaxDescriptorSetUniformBuffersDynamic,
|
||||
MaxDescriptorSetUpdateAfterBindSampledImages =
|
||||
indexing.MaxDescriptorSetUpdateAfterBindSampledImages,
|
||||
|
|
@ -370,8 +373,11 @@ internal static unsafe class VulkanPhysicalDeviceInspector
|
|||
indexing.MaxPerStageDescriptorUpdateAfterBindSampledImages,
|
||||
TimestampComputeAndGraphics = limits.TimestampComputeAndGraphics,
|
||||
MinStorageBufferOffsetAlignment = (uint)limits.MinStorageBufferOffsetAlignment,
|
||||
MaxStorageBufferRange = limits.MaxStorageBufferRange,
|
||||
MinUniformBufferOffsetAlignment = (uint)limits.MinUniformBufferOffsetAlignment,
|
||||
MaxImageDimension2D = limits.MaxImageDimension2D,
|
||||
MaxImageArrayLayers = limits.MaxImageArrayLayers,
|
||||
DeviceLocalHeapBytes = LargestDeviceLocalHeap(vk, device),
|
||||
MaxColorSampleCount = HighestSampleCount(
|
||||
limits.FramebufferColorSampleCounts & limits.FramebufferDepthSampleCounts),
|
||||
};
|
||||
|
|
@ -398,10 +404,28 @@ internal static unsafe class VulkanPhysicalDeviceInspector
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(vk);
|
||||
|
||||
Format depthStencil = ChooseDepthStencilFormat(vk, device);
|
||||
FormatProperties rgba16;
|
||||
vk.GetPhysicalDeviceFormatProperties(device, Format.R16G16B16A16Sfloat, &rgba16);
|
||||
FormatFeatureFlags rgba16Features = rgba16.OptimalTilingFeatures;
|
||||
|
||||
return new VulkanFormatSupport
|
||||
{
|
||||
SwapchainUnormFormat = surfaceOffersUnorm,
|
||||
DepthStencilFormat = ChooseDepthStencilFormat(vk, device),
|
||||
DepthStencilFormat = depthStencil,
|
||||
DepthStencilSampled =
|
||||
depthStencil != Format.Undefined
|
||||
&& SupportsOptimalSampling(vk, device, depthStencil),
|
||||
Rgba16FloatColorAttachment =
|
||||
rgba16Features.HasFlag(FormatFeatureFlags.ColorAttachmentBit),
|
||||
Rgba16FloatSampled =
|
||||
rgba16Features.HasFlag(FormatFeatureFlags.SampledImageBit),
|
||||
Rgba16FloatLinearFilter =
|
||||
rgba16Features.HasFlag(FormatFeatureFlags.SampledImageFilterLinearBit),
|
||||
MaxRgba16FloatSampleCount = ReadOptimalColorSampleCount(
|
||||
vk,
|
||||
device,
|
||||
Format.R16G16B16A16Sfloat),
|
||||
Bc1Sampled = SupportsOptimalSampling(vk, device, Format.BC1RgbaUnormBlock),
|
||||
Bc2Sampled = SupportsOptimalSampling(vk, device, Format.BC2UnormBlock),
|
||||
Bc3Sampled = SupportsOptimalSampling(vk, device, Format.BC3UnormBlock),
|
||||
|
|
@ -436,6 +460,32 @@ internal static unsafe class VulkanPhysicalDeviceInspector
|
|||
return properties.OptimalTilingFeatures.HasFlag(FormatFeatureFlags.SampledImageBit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query the sample-count mask for an optimal-tiling colour attachment.
|
||||
/// Physical-device framebuffer limits are only an upper bound; the concrete
|
||||
/// RGBA16F format may support fewer samples. Sampling is checked separately
|
||||
/// because the multisample image is transient and only its one-sample
|
||||
/// resolve target carries <c>SAMPLED</c> usage.
|
||||
/// </summary>
|
||||
internal static uint ReadOptimalColorSampleCount(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
PhysicalDevice device,
|
||||
Format format)
|
||||
{
|
||||
ImageFormatProperties properties;
|
||||
Result result = vk.GetPhysicalDeviceImageFormatProperties(
|
||||
device,
|
||||
format,
|
||||
ImageType.Type2D,
|
||||
ImageTiling.Optimal,
|
||||
ImageUsageFlags.ColorAttachmentBit,
|
||||
ImageCreateFlags.None,
|
||||
&properties);
|
||||
return result == Result.Success
|
||||
? HighestSampleCount(properties.SampleCounts)
|
||||
: 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate queue families, reporting present support only when a surface
|
||||
/// is supplied. The headless probe passes <c>null</c> and takes the
|
||||
|
|
@ -517,10 +567,12 @@ internal sealed unsafe class VulkanLogicalDeviceFactory
|
|||
Silk.NET.Vulkan.Vk vk,
|
||||
PhysicalDevice physicalDevice,
|
||||
VulkanQueueFamilyChoice families,
|
||||
bool requireSwapchain)
|
||||
bool requireSwapchain,
|
||||
VulkanDeviceFeatureSupport availableFeatures)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(vk);
|
||||
ArgumentNullException.ThrowIfNull(families);
|
||||
ArgumentNullException.ThrowIfNull(availableFeatures);
|
||||
|
||||
IReadOnlyList<string> available =
|
||||
VulkanInterop.EnumerateDeviceExtensions(vk, physicalDevice);
|
||||
|
|
@ -577,6 +629,9 @@ internal sealed unsafe class VulkanLogicalDeviceFactory
|
|||
SType = StructureType.PhysicalDeviceVulkan11Features,
|
||||
PNext = &vulkan12,
|
||||
ShaderDrawParameters = true,
|
||||
// Core 1.1 optional feature: enable it iff the physical-device
|
||||
// Features2 chain reported it. No extension path is attempted.
|
||||
Multiview = availableFeatures.Multiview,
|
||||
};
|
||||
var core = new PhysicalDeviceFeatures
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ using Silk.NET.Vulkan;
|
|||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V, plan §3.4 and §4.4: the three descriptor set layouts and the ONE
|
||||
/// pipeline layout every acdream pipeline shares.
|
||||
/// Campaign V, plan §3.4 and §4.4: retail's three descriptor set layouts and
|
||||
/// shared pipeline layout, plus the strictly opt-in render-pack extension.
|
||||
///
|
||||
/// <para>Extracted from slice V5's active capability probe at V6b so the probe
|
||||
/// and the live backend build the same objects from the same code. The probe's
|
||||
|
|
@ -12,33 +12,110 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// device; a second, similar-looking definition would quietly destroy that
|
||||
/// property the first time one of them changed.</para>
|
||||
///
|
||||
/// <para><b>One pipeline layout is a decision, not an economy.</b> Because every
|
||||
/// pipeline shares it, switching pipelines mid-pass does not invalidate bound
|
||||
/// descriptor sets or push constants — which is what lets the world dispatcher
|
||||
/// bind the texture table once per frame and then change pipeline per bucket.
|
||||
/// The single 96-byte push-constant block exists for the same reason.</para>
|
||||
/// <para><b>Retail remains authoritative.</b> Its layout is exactly sets 0, 1,
|
||||
/// and 2 with the original 96-byte push block. A flagged render-pack pipeline
|
||||
/// lazily acquires a compatible four-set layout whose additional set 3 owns
|
||||
/// bindings 5..8. The pack objects are reference-counted through pipeline
|
||||
/// retirement, so selecting retail creates no Vulkan pack object at all.</para>
|
||||
/// </summary>
|
||||
internal static unsafe class VulkanPipelineLayouts
|
||||
{
|
||||
/// <summary>The three sets plus the shared layout, owned together and destroyed together.</summary>
|
||||
internal sealed class Created(
|
||||
DescriptorSetLayout storage,
|
||||
DescriptorSetLayout uniform,
|
||||
DescriptorSetLayout textureTable,
|
||||
PipelineLayout pipelineLayout) : IDisposable
|
||||
/// <summary>The retail layouts and the lazy lifetime of the optional pack layout.</summary>
|
||||
internal sealed class Created : IDisposable
|
||||
{
|
||||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly object _packLock = new();
|
||||
private PackState? _pack;
|
||||
private int _packReferences;
|
||||
private int _nextPackGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
internal DescriptorSetLayout Storage { get; } = storage;
|
||||
internal DescriptorSetLayout Uniform { get; } = uniform;
|
||||
internal DescriptorSetLayout TextureTable { get; } = textureTable;
|
||||
internal PipelineLayout PipelineLayout { get; } = pipelineLayout;
|
||||
internal Created(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
DescriptorSetLayout storage,
|
||||
DescriptorSetLayout uniform,
|
||||
DescriptorSetLayout textureTable,
|
||||
PipelineLayout pipelineLayout)
|
||||
{
|
||||
_vk = vk;
|
||||
_device = device;
|
||||
Storage = storage;
|
||||
Uniform = uniform;
|
||||
TextureTable = textureTable;
|
||||
PipelineLayout = pipelineLayout;
|
||||
}
|
||||
|
||||
internal DescriptorSetLayout Storage { get; }
|
||||
internal DescriptorSetLayout Uniform { get; }
|
||||
internal DescriptorSetLayout TextureTable { get; }
|
||||
internal PipelineLayout PipelineLayout { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the opt-in layout. The first acquisition creates set 3 and
|
||||
/// its compatible four-set pipeline layout; the last retired pipeline
|
||||
/// destroys them together with every descriptor pool allocated from it.
|
||||
/// </summary>
|
||||
internal PackLayoutLease AcquirePackLayout()
|
||||
{
|
||||
lock (_packLock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_pack ??= CreatePackState(++_nextPackGeneration);
|
||||
_packReferences++;
|
||||
return new PackLayoutLease(this, _pack);
|
||||
}
|
||||
}
|
||||
|
||||
private PackState CreatePackState(int generation)
|
||||
{
|
||||
DescriptorSetLayout packUniform = default;
|
||||
try
|
||||
{
|
||||
packUniform = CreatePackUniformSetLayout(_vk, _device);
|
||||
PipelineLayout packPipeline = CreatePackPipelineLayout(
|
||||
_vk,
|
||||
_device,
|
||||
Storage,
|
||||
Uniform,
|
||||
TextureTable,
|
||||
packUniform);
|
||||
return new PackState(_vk, _device, generation, packUniform, packPipeline);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (packUniform.Handle != 0)
|
||||
_vk.DestroyDescriptorSetLayout(_device, packUniform, null);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleasePackLayout(PackState state)
|
||||
{
|
||||
lock (_packLock)
|
||||
{
|
||||
if (_pack != state || _packReferences <= 0)
|
||||
return;
|
||||
_packReferences--;
|
||||
if (_packReferences != 0)
|
||||
return;
|
||||
_pack = null;
|
||||
state.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
internal void Destroy(Silk.NET.Vulkan.Vk vk, Device device)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
lock (_packLock)
|
||||
{
|
||||
_pack?.Destroy();
|
||||
_pack = null;
|
||||
_packReferences = 0;
|
||||
}
|
||||
if (PipelineLayout.Handle != 0)
|
||||
vk.DestroyPipelineLayout(device, PipelineLayout, null);
|
||||
if (TextureTable.Handle != 0)
|
||||
|
|
@ -51,6 +128,114 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
|
||||
/// <summary>Destruction needs the device, so <see cref="Destroy"/> is the real disposer.</summary>
|
||||
public void Dispose() => _disposed = true;
|
||||
|
||||
internal sealed class PackLayoutLease : IDisposable
|
||||
{
|
||||
private Created? _owner;
|
||||
|
||||
internal PackLayoutLease(Created owner, PackState state)
|
||||
{
|
||||
_owner = owner;
|
||||
State = state;
|
||||
}
|
||||
|
||||
internal PackState State { get; }
|
||||
internal PipelineLayout PipelineLayout => State.PipelineLayout;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Created? owner = Interlocked.Exchange(ref _owner, null);
|
||||
owner?.ReleasePackLayout(State);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One generation of the pack layout and all descriptor pools allocated
|
||||
/// against it. Keeping the pools here prevents a stale per-flight set
|
||||
/// from outliving the descriptor-set layout it was allocated from.
|
||||
/// </summary>
|
||||
internal sealed unsafe class PackState
|
||||
{
|
||||
private const int SetsPerPool = 32;
|
||||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly List<DescriptorPool> _pools = [];
|
||||
private int _setCount;
|
||||
private bool _destroyed;
|
||||
|
||||
internal PackState(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
int generation,
|
||||
DescriptorSetLayout descriptorSetLayout,
|
||||
PipelineLayout pipelineLayout)
|
||||
{
|
||||
_vk = vk;
|
||||
_device = device;
|
||||
Generation = generation;
|
||||
DescriptorSetLayout = descriptorSetLayout;
|
||||
PipelineLayout = pipelineLayout;
|
||||
}
|
||||
|
||||
internal int Generation { get; }
|
||||
internal DescriptorSetLayout DescriptorSetLayout { get; }
|
||||
internal PipelineLayout PipelineLayout { get; }
|
||||
|
||||
internal DescriptorSet AllocateDescriptorSet()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_destroyed, this);
|
||||
if (_setCount % SetsPerPool == 0)
|
||||
_pools.Add(CreatePool());
|
||||
|
||||
DescriptorSetLayout layout = DescriptorSetLayout;
|
||||
var allocate = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = _pools[^1],
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &layout,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
_vk.AllocateDescriptorSets(_device, &allocate, out DescriptorSet set),
|
||||
"vkAllocateDescriptorSets (render-pack set 3)");
|
||||
_setCount++;
|
||||
return set;
|
||||
}
|
||||
|
||||
private DescriptorPool CreatePool()
|
||||
{
|
||||
var size = new DescriptorPoolSize
|
||||
{
|
||||
Type = DescriptorType.UniformBufferDynamic,
|
||||
DescriptorCount = PackUniformBindingCount * SetsPerPool,
|
||||
};
|
||||
var create = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = SetsPerPool,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &size,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
_vk.CreateDescriptorPool(_device, &create, null, out DescriptorPool pool),
|
||||
"vkCreateDescriptorPool (render-pack set 3)");
|
||||
return pool;
|
||||
}
|
||||
|
||||
internal void Destroy()
|
||||
{
|
||||
if (_destroyed)
|
||||
return;
|
||||
_destroyed = true;
|
||||
foreach (DescriptorPool pool in _pools)
|
||||
_vk.DestroyDescriptorPool(_device, pool, null);
|
||||
_pools.Clear();
|
||||
if (PipelineLayout.Handle != 0)
|
||||
_vk.DestroyPipelineLayout(_device, PipelineLayout, null);
|
||||
if (DescriptorSetLayout.Handle != 0)
|
||||
_vk.DestroyDescriptorSetLayout(_device, DescriptorSetLayout, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates all four objects, cleaning up whatever succeeded if a later one fails.</summary>
|
||||
|
|
@ -67,7 +252,7 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
uniform = CreateUniformSetLayout(vk, device);
|
||||
table = CreateTextureTableSetLayout(vk, device);
|
||||
PipelineLayout layout = CreatePipelineLayout(vk, device, storage, uniform, table);
|
||||
return new Created(storage, uniform, table, layout);
|
||||
return new Created(vk, device, storage, uniform, table, layout);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -105,11 +290,10 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
/// at all can fail this layout. That matters for slice V9's lavapipe row and
|
||||
/// for whatever Linux driver the deferred physical row eventually uses.</para>
|
||||
///
|
||||
/// <para><b>Binding 9 is the clearest case.</b> The texture table is the
|
||||
/// GL-only <c>uvec2</c> handle-buffer emulation; the Vulkan backend binds set
|
||||
/// 2 instead and never touches binding 9 at all, so a dynamic descriptor for
|
||||
/// it would be a device resource spent on a binding that is provably never
|
||||
/// bound.</para>
|
||||
/// <para><b>Binding 9 is a plain descriptor.</b> #226 uploads one
|
||||
/// per-instance detail-category array at a stable ring range for each
|
||||
/// submission. It does not need to be re-pointed between draw calls, so a
|
||||
/// scarce dynamic descriptor buys it nothing.</para>
|
||||
///
|
||||
/// <para><b>What to do if V4c disagrees.</b> Bindings 6, 7 and 8 are
|
||||
/// per-instance arrays grouped here with the frame-global tables because
|
||||
|
|
@ -153,8 +337,8 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
|
||||
/// <summary>
|
||||
/// Set 0 — the <see cref="GpuBindingModel.StorageBindingCount"/> storage
|
||||
/// bindings <see cref="GpuBindingModel"/> pins (nine, since Campaign V
|
||||
/// slice V11 deleted the GL-only <c>StorageTextureTable</c> binding), split
|
||||
/// bindings <see cref="GpuBindingModel"/> pins (ten after #226 reclaimed
|
||||
/// binding 9 from the deleted GL-only texture table), split
|
||||
/// between dynamic and plain by <see cref="IsDynamicStorageBinding"/>.
|
||||
/// </summary>
|
||||
internal static DescriptorSetLayout CreateStorageSetLayout(Silk.NET.Vulkan.Vk vk, Device device)
|
||||
|
|
@ -199,6 +383,13 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
/// </summary>
|
||||
internal const uint UniformTerrainClip = 2;
|
||||
|
||||
/// <summary>Bindings 5..8 in opt-in set 3.</summary>
|
||||
internal const uint PackUniformBindingCount = 4;
|
||||
|
||||
internal static bool IsDeclaredPackUniformBinding(uint binding) =>
|
||||
binding >= GpuBindingModel.UniformAtmosphericFrame
|
||||
&& binding <= GpuBindingModel.UniformPackSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Which of set 1's bindings the layout declares — the uniform-side twin of
|
||||
/// <see cref="IsDynamicStorageBinding"/>, and for the same reason: the
|
||||
|
|
@ -247,12 +438,9 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set 1 — the SceneLighting, terrain-clip, terrain-tiling and sky-params
|
||||
/// uniform blocks. All four are dynamic: each is fed from the per-frame ring,
|
||||
/// so its offset moves every frame and a dynamic descriptor is exactly what
|
||||
/// spares the write. Four is half Vulkan's guaranteed
|
||||
/// <c>maxDescriptorSetUniformBuffersDynamic</c> of 8, and the capability gate
|
||||
/// asserts it.
|
||||
/// Set 1 — only retail frame blocks. All four are dynamic: each is fed from
|
||||
/// the per-frame ring, so its offset moves every frame and a dynamic
|
||||
/// descriptor is exactly what spares the write.
|
||||
/// </summary>
|
||||
internal static DescriptorSetLayout CreateUniformSetLayout(Silk.NET.Vulkan.Vk vk, Device device)
|
||||
{
|
||||
|
|
@ -281,6 +469,38 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in set 3 — sparse dynamic uniform bindings 5..8 from render-pack ABI
|
||||
/// v1. It is deliberately not created by <see cref="Create"/>.
|
||||
/// </summary>
|
||||
internal static DescriptorSetLayout CreatePackUniformSetLayout(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device)
|
||||
{
|
||||
DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[(int)PackUniformBindingCount];
|
||||
for (uint i = 0; i < PackUniformBindingCount; i++)
|
||||
{
|
||||
bindings[i] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = GpuBindingModel.UniformAtmosphericFrame + i,
|
||||
DescriptorType = DescriptorType.UniformBufferDynamic,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
};
|
||||
}
|
||||
|
||||
var create = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = PackUniformBindingCount,
|
||||
PBindings = bindings,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateDescriptorSetLayout(device, &create, null, out DescriptorSetLayout layout),
|
||||
"vkCreateDescriptorSetLayout (set 3, render-pack uniforms)");
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set 2 — the production texture table exactly as §4.4 specifies it: one
|
||||
/// combined-image-sampler binding of
|
||||
|
|
@ -323,7 +543,7 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// One shared pipeline layout: three sets plus the single 96-byte
|
||||
/// Retail's shared pipeline layout: three sets plus the single 96-byte
|
||||
/// push-constant block. Creating it proves <c>maxBoundDescriptorSets</c> and
|
||||
/// <c>maxPushConstantsSize</c> for real rather than by reading a limit.
|
||||
/// </summary>
|
||||
|
|
@ -358,4 +578,42 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
"vkCreatePipelineLayout");
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render-pack layout. Sets 0..2 are byte-for-byte the retail layouts; set 3
|
||||
/// adds only the pack uniform ABI, and the push range remains 96 bytes.
|
||||
/// </summary>
|
||||
internal static PipelineLayout CreatePackPipelineLayout(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
DescriptorSetLayout storage,
|
||||
DescriptorSetLayout uniform,
|
||||
DescriptorSetLayout table,
|
||||
DescriptorSetLayout packUniform)
|
||||
{
|
||||
DescriptorSetLayout* sets = stackalloc DescriptorSetLayout[4];
|
||||
sets[0] = storage;
|
||||
sets[1] = uniform;
|
||||
sets[2] = table;
|
||||
sets[3] = packUniform;
|
||||
|
||||
var pushConstants = new PushConstantRange
|
||||
{
|
||||
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
Offset = 0,
|
||||
Size = GpuBindingModel.PushConstantBytes,
|
||||
};
|
||||
var create = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 4,
|
||||
PSetLayouts = sets,
|
||||
PushConstantRangeCount = 1,
|
||||
PPushConstantRanges = &pushConstants,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreatePipelineLayout(device, &create, null, out PipelineLayout layout),
|
||||
"vkCreatePipelineLayout (render-pack ABI)");
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Separates process/device-terminal Vulkan failures from faults contributed by
|
||||
/// an optional render pack. Only the latter may be quarantined to the default
|
||||
/// renderer; a lost device or exhausted host/device memory cannot be made safe
|
||||
/// by changing render graphs.
|
||||
/// </summary>
|
||||
internal static class VulkanRenderFailurePolicy
|
||||
{
|
||||
internal static bool IsFatal(Exception error)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(error);
|
||||
|
||||
if (error is AggregateException aggregate)
|
||||
{
|
||||
foreach (Exception inner in aggregate.Flatten().InnerExceptions)
|
||||
{
|
||||
if (IsFatal(inner))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (Exception? current = error; current is not null; current = current.InnerException)
|
||||
{
|
||||
if (current is OutOfMemoryException)
|
||||
return true;
|
||||
if (current is VulkanCallException vulkan && IsFatal(vulkan.Result))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsFatal(Result result) => result is
|
||||
Result.ErrorDeviceLost
|
||||
or Result.ErrorOutOfHostMemory
|
||||
or Result.ErrorOutOfDeviceMemory
|
||||
// The swapchain policy already treats a lost surface as terminal. It
|
||||
// cannot be repaired by falling back from a pack to the default graph.
|
||||
or Result.ErrorSurfaceLostKhr;
|
||||
}
|
||||
|
|
@ -6,14 +6,11 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// Campaign V slice V6b, plan §4.3: <see cref="GpuTextureFormat"/> to
|
||||
/// <see cref="Format"/>, and the byte arithmetic each format implies.
|
||||
///
|
||||
/// <para>Every format here is UNORM, and that is a finding rather than a
|
||||
/// default. The V3 audit (plan §4.10) established that acdream has no sRGB
|
||||
/// anywhere: not on upload, not in a shader, not at the framebuffer. The plan
|
||||
/// previously specified an sRGB swapchain "matching the GL FramebufferSrgb
|
||||
/// contract" — a contract that does not exist. Shipping an sRGB format would
|
||||
/// have applied an unwanted encode to already-display-space values, brightening
|
||||
/// every frame, and it would have passed silently until the V7 differential.
|
||||
/// </para>
|
||||
/// <para>The retail path's formats are UNORM, and that is a finding rather than
|
||||
/// a default. The V3 audit (plan §4.10) established that acdream has no sRGB
|
||||
/// anywhere: not on upload, not in a shader, not at the framebuffer. The one
|
||||
/// float format in this mapping is an opt-in HDR intermediate; it does not alter
|
||||
/// the retail swapchain or texture decode convention.</para>
|
||||
/// </summary>
|
||||
internal static class VulkanTextureFormatMapping
|
||||
{
|
||||
|
|
@ -54,6 +51,7 @@ internal static class VulkanTextureFormatMapping
|
|||
// Deliberately the same 32-bit UNORM order as the swapchain rather than
|
||||
// literal RGBA — see CanonicalColorAttachmentFormat.
|
||||
GpuTextureFormat.Rgba8UnormRenderTarget => CanonicalColorAttachmentFormat,
|
||||
GpuTextureFormat.Rgba16FloatRenderTarget => Format.R16G16B16A16Sfloat,
|
||||
GpuTextureFormat.Depth24Stencil8 => Format.D24UnormS8Uint,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unknown texture format."),
|
||||
};
|
||||
|
|
@ -62,12 +60,15 @@ internal static class VulkanTextureFormatMapping
|
|||
format == GpuTextureFormat.Depth24Stencil8;
|
||||
|
||||
internal static bool IsRenderTarget(GpuTextureFormat format) =>
|
||||
format is GpuTextureFormat.Rgba8UnormRenderTarget or GpuTextureFormat.Depth24Stencil8;
|
||||
format is GpuTextureFormat.Rgba8UnormRenderTarget
|
||||
or GpuTextureFormat.Rgba16FloatRenderTarget
|
||||
or GpuTextureFormat.Depth24Stencil8;
|
||||
|
||||
/// <summary>Bytes one texel occupies. Only meaningful for uncompressed formats.</summary>
|
||||
internal static int BytesPerTexel(GpuTextureFormat format) => format switch
|
||||
{
|
||||
GpuTextureFormat.Rgba8Unorm or GpuTextureFormat.Rgba8UnormRenderTarget => 4,
|
||||
GpuTextureFormat.Rgba16FloatRenderTarget => 8,
|
||||
GpuTextureFormat.R8Unorm => 1,
|
||||
GpuTextureFormat.Depth24Stencil8 => 4,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "A block-compressed format has no texel size."),
|
||||
|
|
|
|||
|
|
@ -110,9 +110,11 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
|
|||
private readonly VulkanTextureSlotAllocator _slots;
|
||||
private readonly DescriptorPool _pool;
|
||||
private readonly DescriptorSet _set;
|
||||
private readonly object _sync = new();
|
||||
|
||||
private ImageView _defaultView;
|
||||
private Sampler _defaultSampler;
|
||||
private ImageLayout _defaultLayout = ImageLayout.ShaderReadOnlyOptimal;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanTextureTable(
|
||||
|
|
@ -171,27 +173,51 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
|
|||
|
||||
internal DescriptorSet Set => _set;
|
||||
|
||||
internal int LiveSlotCount => _slots.LiveCount;
|
||||
internal int LiveSlotCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _slots.LiveCount;
|
||||
}
|
||||
}
|
||||
|
||||
internal uint HighWater => _slots.HighWater;
|
||||
internal uint HighWater
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _slots.HighWater;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the (view, sampler) pair written into a slot when it is scrubbed.
|
||||
/// Supplied after the default texture exists, which is necessarily after the
|
||||
/// table itself.
|
||||
/// </summary>
|
||||
internal void SetScrubTarget(ImageView view, Sampler sampler)
|
||||
internal void SetScrubTarget(
|
||||
ImageView view,
|
||||
Sampler sampler,
|
||||
ImageLayout layout = ImageLayout.ShaderReadOnlyOptimal)
|
||||
{
|
||||
_defaultView = view;
|
||||
_defaultSampler = sampler;
|
||||
_defaultLayout = layout;
|
||||
}
|
||||
|
||||
internal GpuTextureSlot Register(ImageView view, Sampler sampler)
|
||||
internal GpuTextureSlot Register(
|
||||
ImageView view,
|
||||
Sampler sampler,
|
||||
ImageLayout layout = ImageLayout.ShaderReadOnlyOptimal)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
uint slot = _slots.Allocate();
|
||||
Write(slot, view, sampler);
|
||||
return new GpuTextureSlot(slot);
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
uint slot = _slots.Allocate();
|
||||
Write(slot, view, sampler, layout);
|
||||
return new GpuTextureSlot(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -201,22 +227,33 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
|
|||
/// </summary>
|
||||
internal void ReleaseNow(GpuTextureSlot slot)
|
||||
{
|
||||
if (_disposed || !slot.IsAssigned)
|
||||
return;
|
||||
if (_defaultView.Handle != 0 && _defaultSampler.Handle != 0)
|
||||
Write(slot.Index, _defaultView, _defaultSampler);
|
||||
_slots.Release(slot.Index);
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed || !slot.IsAssigned)
|
||||
return;
|
||||
if (_defaultView.Handle != 0 && _defaultSampler.Handle != 0)
|
||||
Write(slot.Index, _defaultView, _defaultSampler, _defaultLayout);
|
||||
_slots.Release(slot.Index);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsLive(GpuTextureSlot slot) => slot.IsAssigned && _slots.IsLive(slot.Index);
|
||||
internal bool IsLive(GpuTextureSlot slot)
|
||||
{
|
||||
lock (_sync)
|
||||
return slot.IsAssigned && _slots.IsLive(slot.Index);
|
||||
}
|
||||
|
||||
private void Write(uint slot, ImageView view, Sampler sampler)
|
||||
private void Write(
|
||||
uint slot,
|
||||
ImageView view,
|
||||
Sampler sampler,
|
||||
ImageLayout layout)
|
||||
{
|
||||
var info = new DescriptorImageInfo
|
||||
{
|
||||
ImageView = view,
|
||||
Sampler = sampler,
|
||||
ImageLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||
ImageLayout = layout,
|
||||
};
|
||||
var write = new WriteDescriptorSet
|
||||
{
|
||||
|
|
@ -233,10 +270,13 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
if (_pool.Handle != 0)
|
||||
_vk.DestroyDescriptorPool(_device, _pool, null);
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
if (_pool.Handle != 0)
|
||||
_vk.DestroyDescriptorPool(_device, _pool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ internal static class VulkanViewportMapping
|
|||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||
// ACRender::SetDetailSurfaceInternal with DrawBuilding/DrawEnvCell's
|
||||
// category state: D3DBLEND_DESTCOLOR + D3DBLEND_INVSRCALPHA.
|
||||
GpuBlendMode.RetailDetail => (BlendFactor.DstColor, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(blend), blend, "Unknown blend mode."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,7 +52,24 @@ internal sealed unsafe class VulkanWorldPassScope : IWorldPassScope
|
|||
/// per frame, so a nested publication could only mean two phases believe they
|
||||
/// own the frame.
|
||||
/// </summary>
|
||||
public IDisposable Publish(IGpuPassEncoder encoder)
|
||||
public IDisposable Publish(IGpuPassEncoder encoder) =>
|
||||
PublishCore(encoder, preservePreparedSections: false);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a world pass after <see cref="WorldSceneRenderer.PrepareEnhanced"/>
|
||||
/// has already built the frame. That preparation writes the authoritative
|
||||
/// scene-lighting ring section before the shadow passes can run; clearing it
|
||||
/// here would make every HDR receiver bind the zero fallback. The preceding
|
||||
/// publication's disposal (or construction for the first frame) already
|
||||
/// established an empty section set, so this preserves only values prepared
|
||||
/// for the current GPU frame.
|
||||
/// </summary>
|
||||
internal IDisposable PublishPrepared(IGpuPassEncoder encoder) =>
|
||||
PublishCore(encoder, preservePreparedSections: true);
|
||||
|
||||
private IDisposable PublishCore(
|
||||
IGpuPassEncoder encoder,
|
||||
bool preservePreparedSections)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
if (_encoder is not null)
|
||||
|
|
@ -62,7 +79,8 @@ internal sealed unsafe class VulkanWorldPassScope : IWorldPassScope
|
|||
}
|
||||
|
||||
_encoder = encoder;
|
||||
Sections.Reset();
|
||||
if (!preservePreparedSections)
|
||||
Sections.Reset();
|
||||
return _publication;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
internal enum AtmosphericQualityLevel : byte
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
internal readonly record struct AtmosphericQualityMeasurement(
|
||||
double InclusivePackGpuMillisecondsP99,
|
||||
double IncrementalCpuMillisecondsP99,
|
||||
long ResidentGpuBytes,
|
||||
bool StableFrameBoundary);
|
||||
|
||||
internal readonly record struct AtmosphericAutoQualitySnapshot(
|
||||
AtmosphericQualityLevel Current,
|
||||
int ConsecutiveOverBudgetFrames,
|
||||
int ConsecutiveHeadroomFrames,
|
||||
int CooldownFramesRemaining,
|
||||
long ChangeGeneration,
|
||||
bool SafeFallbackToRetailRequested);
|
||||
|
||||
internal readonly record struct AtmosphericQualityBudget(
|
||||
double GpuMillisecondsP99,
|
||||
double CpuMillisecondsP99,
|
||||
long ResidentGpuBytes)
|
||||
{
|
||||
internal static AtmosphericQualityBudget FromPreset(RenderQualityPreset preset) => new(
|
||||
preset.MaxIncrementalGpuMillisecondsP99,
|
||||
preset.MaxIncrementalCpuMillisecondsP99,
|
||||
preset.MaxResidentGpuBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Long-hysteresis automatic quality policy. It changes only resolution,
|
||||
/// cascade count/reach, and post-process sampling through one stable preset
|
||||
/// swap. If even Low remains over its declared budget, it requests an atomic
|
||||
/// whole-pack fallback to retail instead of silently dropping caster classes.
|
||||
/// </summary>
|
||||
internal sealed class AtmosphericAutoQualityController
|
||||
{
|
||||
internal const int DowngradeHysteresisFrames = 180;
|
||||
internal const int UpgradeHysteresisFrames = 900;
|
||||
internal const int ChangeCooldownFrames = 300;
|
||||
|
||||
private AtmosphericQualityLevel _current;
|
||||
private readonly AtmosphericQualityLevel _minimum;
|
||||
private readonly AtmosphericQualityLevel _maximum;
|
||||
private readonly AtmosphericQualityBudget[] _budgets;
|
||||
private int _overBudget;
|
||||
private int _headroom;
|
||||
private int _cooldown;
|
||||
private long _generation;
|
||||
private bool _safeFallbackToRetailRequested;
|
||||
|
||||
internal AtmosphericAutoQualityController(
|
||||
AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium,
|
||||
AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low,
|
||||
AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High)
|
||||
: this(DefaultBudgets(), initial, minimum, maximum)
|
||||
{
|
||||
}
|
||||
|
||||
internal AtmosphericAutoQualityController(
|
||||
IReadOnlyList<AtmosphericQualityBudget> budgets,
|
||||
AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium,
|
||||
AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low,
|
||||
AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(budgets);
|
||||
if (budgets.Count != 3)
|
||||
throw new ArgumentException("Auto quality requires Low, Medium, and High budgets.", nameof(budgets));
|
||||
if (minimum > initial || initial > maximum)
|
||||
throw new ArgumentOutOfRangeException(nameof(initial));
|
||||
_budgets = budgets.ToArray();
|
||||
foreach (AtmosphericQualityBudget budget in _budgets)
|
||||
{
|
||||
if (!double.IsFinite(budget.GpuMillisecondsP99)
|
||||
|| budget.GpuMillisecondsP99 < 0d
|
||||
|| !double.IsFinite(budget.CpuMillisecondsP99)
|
||||
|| budget.CpuMillisecondsP99 < 0d
|
||||
|| budget.ResidentGpuBytes < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(budgets),
|
||||
"Automatic-quality budgets must be finite and non-negative.");
|
||||
}
|
||||
}
|
||||
_minimum = minimum;
|
||||
_maximum = maximum;
|
||||
_current = initial;
|
||||
}
|
||||
|
||||
internal AtmosphericAutoQualitySnapshot Snapshot => new(
|
||||
_current,
|
||||
_overBudget,
|
||||
_headroom,
|
||||
_cooldown,
|
||||
_generation,
|
||||
_safeFallbackToRetailRequested);
|
||||
|
||||
internal AtmosphericQualityBudget CurrentBudget => _budgets[(int)_current];
|
||||
|
||||
internal AtmosphericAutoQualitySnapshot Observe(
|
||||
in AtmosphericQualityMeasurement measurement)
|
||||
{
|
||||
Validate(in measurement);
|
||||
if (!measurement.StableFrameBoundary)
|
||||
return Snapshot;
|
||||
if (_safeFallbackToRetailRequested)
|
||||
return Snapshot;
|
||||
if (_cooldown > 0)
|
||||
{
|
||||
_cooldown--;
|
||||
_overBudget = 0;
|
||||
_headroom = 0;
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
AtmosphericQualityBudget budget = _budgets[(int)_current];
|
||||
bool over = measurement.InclusivePackGpuMillisecondsP99
|
||||
> budget.GpuMillisecondsP99
|
||||
|| measurement.IncrementalCpuMillisecondsP99
|
||||
> budget.CpuMillisecondsP99
|
||||
|| measurement.ResidentGpuBytes > budget.ResidentGpuBytes;
|
||||
if (over)
|
||||
{
|
||||
_overBudget++;
|
||||
_headroom = 0;
|
||||
if (_overBudget >= DowngradeHysteresisFrames)
|
||||
{
|
||||
if (_current != _minimum)
|
||||
Change((AtmosphericQualityLevel)((int)_current - 1));
|
||||
else
|
||||
RequestSafeFallback();
|
||||
}
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
_overBudget = 0;
|
||||
if (_current == _maximum)
|
||||
{
|
||||
_headroom = 0;
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
AtmosphericQualityLevel next =
|
||||
(AtmosphericQualityLevel)((int)_current + 1);
|
||||
AtmosphericQualityBudget nextBudget = _budgets[(int)next];
|
||||
bool hasHeadroom = measurement.InclusivePackGpuMillisecondsP99
|
||||
<= nextBudget.GpuMillisecondsP99 * 0.70
|
||||
&& measurement.IncrementalCpuMillisecondsP99
|
||||
<= nextBudget.CpuMillisecondsP99 * 0.70
|
||||
&& measurement.ResidentGpuBytes
|
||||
<= (long)(nextBudget.ResidentGpuBytes * 0.70);
|
||||
if (!hasHeadroom)
|
||||
{
|
||||
_headroom = 0;
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
_headroom++;
|
||||
if (_headroom >= UpgradeHysteresisFrames)
|
||||
Change(next);
|
||||
return Snapshot;
|
||||
}
|
||||
|
||||
internal void Reset(AtmosphericQualityLevel level)
|
||||
{
|
||||
_current = level;
|
||||
_overBudget = 0;
|
||||
_headroom = 0;
|
||||
_cooldown = 0;
|
||||
_safeFallbackToRetailRequested = false;
|
||||
_generation = checked(_generation + 1);
|
||||
}
|
||||
|
||||
private void Change(AtmosphericQualityLevel value)
|
||||
{
|
||||
_current = value;
|
||||
_overBudget = 0;
|
||||
_headroom = 0;
|
||||
_cooldown = ChangeCooldownFrames;
|
||||
_generation = checked(_generation + 1);
|
||||
}
|
||||
|
||||
private void RequestSafeFallback()
|
||||
{
|
||||
_overBudget = DowngradeHysteresisFrames;
|
||||
_headroom = 0;
|
||||
_cooldown = 0;
|
||||
_safeFallbackToRetailRequested = true;
|
||||
_generation = checked(_generation + 1);
|
||||
}
|
||||
|
||||
private static AtmosphericQualityBudget[] DefaultBudgets() =>
|
||||
[
|
||||
From(DirectionalShadowPreset.Low),
|
||||
From(DirectionalShadowPreset.Medium),
|
||||
From(DirectionalShadowPreset.High),
|
||||
];
|
||||
|
||||
private static AtmosphericQualityBudget From(DirectionalShadowPreset preset)
|
||||
{
|
||||
DirectionalShadowQuality quality = DirectionalShadowQuality.For(preset);
|
||||
return new AtmosphericQualityBudget(
|
||||
quality.IncrementalGpuP99BudgetMilliseconds,
|
||||
quality.IncrementalCpuP99BudgetMilliseconds,
|
||||
quality.PackResidentGpuByteBudget);
|
||||
}
|
||||
|
||||
private static void Validate(in AtmosphericQualityMeasurement value)
|
||||
{
|
||||
if (!double.IsFinite(value.InclusivePackGpuMillisecondsP99)
|
||||
|| value.InclusivePackGpuMillisecondsP99 < 0
|
||||
|| !double.IsFinite(value.IncrementalCpuMillisecondsP99)
|
||||
|| value.IncrementalCpuMillisecondsP99 < 0
|
||||
|| value.ResidentGpuBytes < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
"Atmospheric quality measurements must be finite and non-negative.");
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/AcDream.App/Rendering/Packs/AtmosphericCpuStageProfiler.cs
Normal file
153
src/AcDream.App/Rendering/Packs/AtmosphericCpuStageProfiler.cs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.App.Diagnostics;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal readonly record struct AtmosphericCpuStageFrame(
|
||||
long FrameSerial,
|
||||
long ShadowCasterBuildTicks,
|
||||
long ShadowEnvironmentTicks,
|
||||
long ShadowPreparedDrawsAndTransformsTicks,
|
||||
long ShadowFitAndUniformTicks,
|
||||
long ShadowLayeredPassRecordingTicks,
|
||||
long ShadowBookkeepingTicks,
|
||||
long PostSetupAndOtherTicks,
|
||||
long PostSunRaysTicks,
|
||||
long PostFilmicTicks);
|
||||
|
||||
internal readonly record struct RenderPackCpuStageDiagnostics(
|
||||
string Stage,
|
||||
int SampleCount,
|
||||
double CpuMillisecondsP50,
|
||||
double CpuMillisecondsP95,
|
||||
double CpuMillisecondsP99);
|
||||
|
||||
/// <summary>
|
||||
/// Temporary Low-only structural profiler for the incremental CPU budget. It
|
||||
/// samples the same one-in-four frames as Low GPU timestamps, keeping fewer
|
||||
/// than half of the ordinary performance window instrumented while retaining
|
||||
/// enough observations for a short physical run. Every hot-path buffer is
|
||||
/// fixed at construction and observation is allocation-free.
|
||||
/// </summary>
|
||||
internal sealed class AtmosphericCpuStageProfiler
|
||||
{
|
||||
internal const int SampleIntervalFrames = AtmosphericGpuTimerSampling.LowIntervalFrames;
|
||||
|
||||
private static readonly string[] StageNames =
|
||||
[
|
||||
"target-preparation",
|
||||
"shadow-caster-build",
|
||||
"shadow-environment",
|
||||
"shadow-prepared-draws-and-transforms",
|
||||
"shadow-fit-and-uniform",
|
||||
"shadow-layered-pass-recording",
|
||||
"shadow-bookkeeping",
|
||||
"post-setup-and-other",
|
||||
"post-sun-rays",
|
||||
"post-filmic",
|
||||
"performance-observe-bookkeeping",
|
||||
"measured-pack-total",
|
||||
"measured-pack-unattributed",
|
||||
];
|
||||
|
||||
private readonly FrameStatsBuffer[] _microseconds;
|
||||
|
||||
internal AtmosphericCpuStageProfiler(int capacity = RenderPackPerformanceWindow.DefaultCapacity)
|
||||
{
|
||||
_microseconds = new FrameStatsBuffer[StageNames.Length];
|
||||
for (int i = 0; i < _microseconds.Length; i++)
|
||||
_microseconds[i] = new FrameStatsBuffer(capacity);
|
||||
}
|
||||
|
||||
internal static bool ShouldMeasure(long frameSerial)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSerial);
|
||||
return frameSerial % SampleIntervalFrames == 0;
|
||||
}
|
||||
|
||||
internal void Observe(
|
||||
in AtmosphericCpuStageFrame frame,
|
||||
long targetPreparationTicks,
|
||||
long measuredPackTotalTicks,
|
||||
long observeBookkeepingTicks)
|
||||
{
|
||||
if (frame.FrameSerial <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(frame));
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(targetPreparationTicks);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(measuredPackTotalTicks);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(observeBookkeepingTicks);
|
||||
|
||||
long attributedTicks = checked(
|
||||
targetPreparationTicks
|
||||
+ frame.ShadowCasterBuildTicks
|
||||
+ frame.ShadowEnvironmentTicks
|
||||
+ frame.ShadowPreparedDrawsAndTransformsTicks
|
||||
+ frame.ShadowFitAndUniformTicks
|
||||
+ frame.ShadowLayeredPassRecordingTicks
|
||||
+ frame.ShadowBookkeepingTicks
|
||||
+ frame.PostSetupAndOtherTicks
|
||||
+ frame.PostSunRaysTicks
|
||||
+ frame.PostFilmicTicks);
|
||||
long unattributedTicks = Math.Max(0L, measuredPackTotalTicks - attributedTicks);
|
||||
|
||||
Push(0, targetPreparationTicks);
|
||||
Push(1, frame.ShadowCasterBuildTicks);
|
||||
Push(2, frame.ShadowEnvironmentTicks);
|
||||
Push(3, frame.ShadowPreparedDrawsAndTransformsTicks);
|
||||
Push(4, frame.ShadowFitAndUniformTicks);
|
||||
Push(5, frame.ShadowLayeredPassRecordingTicks);
|
||||
Push(6, frame.ShadowBookkeepingTicks);
|
||||
Push(7, frame.PostSetupAndOtherTicks);
|
||||
Push(8, frame.PostSunRaysTicks);
|
||||
Push(9, frame.PostFilmicTicks);
|
||||
Push(10, observeBookkeepingTicks);
|
||||
Push(11, measuredPackTotalTicks);
|
||||
Push(12, unattributedTicks);
|
||||
}
|
||||
|
||||
internal IReadOnlyList<RenderPackCpuStageDiagnostics> Snapshot()
|
||||
{
|
||||
var result = new RenderPackCpuStageDiagnostics[StageNames.Length];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
FrameStatsBuffer samples = _microseconds[i];
|
||||
result[i] = new RenderPackCpuStageDiagnostics(
|
||||
StageNames[i],
|
||||
samples.Count,
|
||||
samples.Percentile(0.50) / 1000d,
|
||||
samples.Percentile(0.95) / 1000d,
|
||||
samples.Percentile(0.99) / 1000d);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
for (int i = 0; i < _microseconds.Length; i++)
|
||||
_microseconds[i].Reset();
|
||||
}
|
||||
|
||||
private void Push(int stage, long ticks)
|
||||
{
|
||||
long microseconds = checked((long)Math.Round(
|
||||
ticks * 1_000_000d / Stopwatch.Frequency,
|
||||
MidpointRounding.AwayFromZero));
|
||||
_microseconds[stage].Push(microseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional production-frame seam. Only Low's built-in graph implements it;
|
||||
/// retail, Medium, High, and declared graphs never enter the profiling path.
|
||||
/// </summary>
|
||||
internal interface IAtmosphericCpuStageProfileRuntime
|
||||
{
|
||||
bool ShouldProfileCpuFrame(long frameSerial);
|
||||
|
||||
void CompleteCpuProfile(
|
||||
long frameSerial,
|
||||
long targetPreparationTicks,
|
||||
long measuredPackTotalTicks,
|
||||
long observeBookkeepingTicks,
|
||||
bool stableFrameBoundary);
|
||||
}
|
||||
229
src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs
Normal file
229
src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable authored atmosphere and exact camera projection captured from the
|
||||
/// normal world frame. This value owns no gameplay or renderer objects and is
|
||||
/// valid after the wrapped world renderer returns.
|
||||
/// </summary>
|
||||
internal readonly record struct AtmosphericFrameInputs(
|
||||
Vector2 SunScreenUv,
|
||||
bool SunIsOnScreen,
|
||||
float SunElevationDegrees,
|
||||
Vector3 SunColor,
|
||||
Vector3 SunDirection,
|
||||
float SunDirectionalBrightness,
|
||||
Matrix4x4 InverseViewProjection,
|
||||
int ActiveDayGroup,
|
||||
WeatherKind Weather,
|
||||
float WeatherIntensity,
|
||||
double DeltaSeconds,
|
||||
int ViewportWidth,
|
||||
int ViewportHeight,
|
||||
bool IsOutdoor);
|
||||
|
||||
internal interface IAtmosphericWorldFrameSink
|
||||
{
|
||||
void Publish(
|
||||
in RenderFrameFoundation foundation,
|
||||
in WorldRenderFrame world,
|
||||
int activeDayGroup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-frame handoff between <see cref="WorldSceneRenderer"/>, which owns the
|
||||
/// canonical camera build, and the post graph. Reset happens before the world
|
||||
/// pass so an intentionally skipped world can never reuse a prior camera.
|
||||
/// </summary>
|
||||
internal sealed class AtmosphericFrameInputState : IAtmosphericWorldFrameSink
|
||||
{
|
||||
private RenderFrameInput _host;
|
||||
private RenderFrameFoundation _foundation;
|
||||
private AtmosphericFrameInputs _current;
|
||||
private bool _published;
|
||||
|
||||
internal void BeginFrame(
|
||||
in RenderFrameInput host,
|
||||
in RenderFrameFoundation foundation)
|
||||
{
|
||||
_host = host;
|
||||
_foundation = foundation;
|
||||
_current = default;
|
||||
_published = false;
|
||||
}
|
||||
|
||||
public void Publish(
|
||||
in RenderFrameFoundation foundation,
|
||||
in WorldRenderFrame world,
|
||||
int activeDayGroup)
|
||||
{
|
||||
Vector3 direction = SkyStateProvider.SunDirectionFromKeyframe(foundation.Sky);
|
||||
Vector3 sunPoint = world.Camera.Position + (direction * 10_000f);
|
||||
Vector4 clip = Vector4.Transform(
|
||||
new Vector4(sunPoint, 1f),
|
||||
world.Camera.ViewProjection);
|
||||
bool finite = float.IsFinite(clip.X)
|
||||
&& float.IsFinite(clip.Y)
|
||||
&& float.IsFinite(clip.W)
|
||||
&& clip.W > 1e-5f;
|
||||
Vector2 uv = finite
|
||||
? new Vector2(
|
||||
(clip.X / clip.W * 0.5f) + 0.5f,
|
||||
0.5f - (clip.Y / clip.W * 0.5f))
|
||||
: new Vector2(-1f, -1f);
|
||||
bool onScreen = finite
|
||||
&& uv.X >= 0f && uv.X <= 1f
|
||||
&& uv.Y >= 0f && uv.Y <= 1f;
|
||||
Matrix4x4 inverseViewProjection = Matrix4x4.Invert(
|
||||
world.Camera.ViewProjection,
|
||||
out Matrix4x4 inverse)
|
||||
? inverse
|
||||
: Matrix4x4.Identity;
|
||||
|
||||
_current = new AtmosphericFrameInputs(
|
||||
uv,
|
||||
onScreen,
|
||||
foundation.Sky.SunPitchDeg,
|
||||
foundation.Sky.SunColor,
|
||||
direction,
|
||||
foundation.Sky.DirBright,
|
||||
inverseViewProjection,
|
||||
activeDayGroup,
|
||||
foundation.Atmosphere.Kind,
|
||||
Math.Clamp(foundation.Atmosphere.Intensity, 0f, 1f),
|
||||
_host.DeltaSeconds,
|
||||
_host.ViewportWidth,
|
||||
_host.ViewportHeight,
|
||||
IsOutdoor: world.Roots.RenderSky && !world.Roots.CameraInsideCell);
|
||||
_published = true;
|
||||
}
|
||||
|
||||
internal AtmosphericFrameInputs Snapshot()
|
||||
{
|
||||
if (_published)
|
||||
return _current;
|
||||
|
||||
// A portal/login frame deliberately skipped the normal world. Preserve
|
||||
// its authored colour inputs but suppress every directional effect.
|
||||
return new AtmosphericFrameInputs(
|
||||
new Vector2(-1f, -1f),
|
||||
SunIsOnScreen: false,
|
||||
_foundation.Sky.SunPitchDeg,
|
||||
_foundation.Sky.SunColor,
|
||||
SkyStateProvider.SunDirectionFromKeyframe(_foundation.Sky),
|
||||
_foundation.Sky.DirBright,
|
||||
Matrix4x4.Identity,
|
||||
-1,
|
||||
_foundation.Atmosphere.Kind,
|
||||
Math.Clamp(_foundation.Atmosphere.Intensity, 0f, 1f),
|
||||
_host.DeltaSeconds,
|
||||
_host.ViewportWidth,
|
||||
_host.ViewportHeight,
|
||||
IsOutdoor: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shader ABI SSOT for opt-in set 3 binding 5. Six std140 vec4 values followed by one
|
||||
/// mat4, 160 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct AtmosphericFrameUniforms
|
||||
{
|
||||
internal const int SizeInBytes = 160;
|
||||
|
||||
internal AtmosphericFrameUniforms(
|
||||
Vector4 sunScreen,
|
||||
Vector4 sunColor,
|
||||
Vector4 viewport,
|
||||
Vector4 weather,
|
||||
Vector4 sunDirection,
|
||||
Vector4 policy,
|
||||
Matrix4x4 inverseViewProjection)
|
||||
{
|
||||
SunScreen = sunScreen;
|
||||
SunColor = sunColor;
|
||||
Viewport = viewport;
|
||||
Weather = weather;
|
||||
SunDirection = sunDirection;
|
||||
Policy = policy;
|
||||
InverseViewProjection = inverseViewProjection;
|
||||
}
|
||||
|
||||
internal readonly Vector4 SunScreen;
|
||||
internal readonly Vector4 SunColor;
|
||||
internal readonly Vector4 Viewport;
|
||||
internal readonly Vector4 Weather;
|
||||
internal readonly Vector4 SunDirection;
|
||||
internal readonly Vector4 Policy;
|
||||
internal readonly Matrix4x4 InverseViewProjection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shader ABI SSOT for opt-in set 3 binding 7. Passes assign meanings to four std140
|
||||
/// vec4 values without changing the shared descriptor layout.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct AtmosphericPackPassUniforms
|
||||
{
|
||||
internal const int SizeInBytes = 64;
|
||||
|
||||
internal AtmosphericPackPassUniforms(
|
||||
Vector4 params0,
|
||||
Vector4 params1,
|
||||
Vector4 params2,
|
||||
Vector4 params3)
|
||||
{
|
||||
Params0 = params0;
|
||||
Params1 = params1;
|
||||
Params2 = params2;
|
||||
Params3 = params3;
|
||||
}
|
||||
|
||||
internal readonly Vector4 Params0;
|
||||
internal readonly Vector4 Params1;
|
||||
internal readonly Vector4 Params2;
|
||||
internal readonly Vector4 Params3;
|
||||
|
||||
internal static AtmosphericPackPassUniforms From(Vector4 params0) =>
|
||||
new(params0, Vector4.Zero, Vector4.Zero, Vector4.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shader ABI SSOT for opt-in set 3 binding 8. API v1 exposes 64 scalar values in
|
||||
/// descriptor declaration order, physically grouped as sixteen std140 vec4s.
|
||||
/// </summary>
|
||||
[InlineArray(RenderPackShaderAbi.PackSettingScalarCapacity)]
|
||||
internal struct PackSettingsUniforms
|
||||
{
|
||||
internal const int SizeInBytes = RenderPackShaderAbi.PackSettingsSizeBytes;
|
||||
private float _element0;
|
||||
|
||||
internal static PackSettingsUniforms Create(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
{
|
||||
var result = new PackSettingsUniforms();
|
||||
int count = Math.Min(
|
||||
descriptor.Settings.Count,
|
||||
RenderPackShaderAbi.PackSettingScalarCapacity);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
RenderSettingDeclaration setting = descriptor.Settings[i];
|
||||
string value = RenderPackSettingResolution.Resolve(
|
||||
setting,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
result[i] = RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
|
||||
? encoded
|
||||
: 0f;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed per-pass GPU timestamps are diagnostic commands, not visual work.
|
||||
/// Low samples one complete frame in four so its tight median CPU budget is not
|
||||
/// dominated by instrumentation; sampled frames still include every receiver,
|
||||
/// shadow, and post-process scope and therefore preserve the inclusive GPU
|
||||
/// measurement contract. Medium and High retain continuous measurement.
|
||||
/// </summary>
|
||||
internal static class AtmosphericGpuTimerSampling
|
||||
{
|
||||
internal const int LowIntervalFrames = 4;
|
||||
|
||||
internal static bool ShouldMeasure(
|
||||
RenderQualitySemantic quality,
|
||||
long frameSerial)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSerial);
|
||||
return quality is not RenderQualitySemantic.Low
|
||||
|| frameSerial % LowIntervalFrames == 0;
|
||||
}
|
||||
}
|
||||
1731
src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs
Normal file
1731
src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs
Normal file
File diff suppressed because it is too large
Load diff
183
src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs
Normal file
183
src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Pack-only identity for the one celestial direction selected to cast the
|
||||
/// current directional shadow map. Retail exposes one authored directional
|
||||
/// colour/energy channel; moon meshes contribute direction only.
|
||||
/// </summary>
|
||||
internal enum AuthoredCelestialShadowSourceKind : uint
|
||||
{
|
||||
None = 0,
|
||||
Sun = 1,
|
||||
DominantMoon = 2,
|
||||
SecondaryMoon = 3,
|
||||
}
|
||||
|
||||
internal readonly record struct AuthoredCelestialShadowSource(
|
||||
AuthoredCelestialShadowSourceKind Kind,
|
||||
int ObjectIndex,
|
||||
uint GfxObjId,
|
||||
Vector3 SurfaceToLightDirection,
|
||||
float ElevationSin,
|
||||
float AuthoredEnergy)
|
||||
{
|
||||
internal static AuthoredCelestialShadowSource None(float authoredEnergy = 0f) =>
|
||||
new(
|
||||
AuthoredCelestialShadowSourceKind.None,
|
||||
-1,
|
||||
0u,
|
||||
Vector3.UnitZ,
|
||||
0f,
|
||||
Math.Clamp(authoredEnergy, 0f, 1f));
|
||||
|
||||
internal bool IsAvailable =>
|
||||
Kind is not AuthoredCelestialShadowSourceKind.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the visible Dereth sun/moons from retail DAT sky objects and uses
|
||||
/// the identical transform as <c>SkyRenderer</c>. This is an opt-in render-pack
|
||||
/// enhancement; it never changes retail SceneLighting or world state.
|
||||
/// </summary>
|
||||
internal static class AuthoredCelestialShadowSourceResolver
|
||||
{
|
||||
internal const uint SunGfxObjId = 0x01001348u;
|
||||
internal const uint DominantMoonGfxObjId = 0x01001F6Au;
|
||||
internal const uint SecondaryMoonGfxObjId = 0x01001F67u;
|
||||
|
||||
internal static AuthoredCelestialShadowSource Resolve(
|
||||
DayGroupData? dayGroup,
|
||||
float dayFraction,
|
||||
in SkyKeyframe sky)
|
||||
{
|
||||
float energy = Math.Clamp(
|
||||
MathF.Max(sky.SunColor.X, MathF.Max(sky.SunColor.Y, sky.SunColor.Z)),
|
||||
0f,
|
||||
1f);
|
||||
if (dayGroup is null || !float.IsFinite(dayFraction))
|
||||
return AuthoredCelestialShadowSource.None(energy);
|
||||
|
||||
if (TryResolve(
|
||||
dayGroup,
|
||||
dayFraction,
|
||||
SunGfxObjId,
|
||||
AuthoredCelestialShadowSourceKind.Sun,
|
||||
energy,
|
||||
out var source)
|
||||
|| TryResolve(
|
||||
dayGroup,
|
||||
dayFraction,
|
||||
DominantMoonGfxObjId,
|
||||
AuthoredCelestialShadowSourceKind.DominantMoon,
|
||||
energy,
|
||||
out source)
|
||||
|| TryResolve(
|
||||
dayGroup,
|
||||
dayFraction,
|
||||
SecondaryMoonGfxObjId,
|
||||
AuthoredCelestialShadowSourceKind.SecondaryMoon,
|
||||
energy,
|
||||
out source))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
return AuthoredCelestialShadowSource.None(energy);
|
||||
}
|
||||
|
||||
private static bool TryResolve(
|
||||
DayGroupData dayGroup,
|
||||
float dayFraction,
|
||||
uint roleGfxObjId,
|
||||
AuthoredCelestialShadowSourceKind kind,
|
||||
float energy,
|
||||
out AuthoredCelestialShadowSource source)
|
||||
{
|
||||
for (int index = 0; index < dayGroup.SkyObjects.Count; index++)
|
||||
{
|
||||
SkyObjectData skyObject = dayGroup.SkyObjects[index];
|
||||
if (skyObject.GfxObjId != roleGfxObjId
|
||||
|| !skyObject.IsVisible(dayFraction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SkyObjectReplaceData? replace = ActiveReplace(
|
||||
dayGroup,
|
||||
dayFraction,
|
||||
checked((uint)index));
|
||||
if (replace is not null && replace.Transparent >= 1f - 1e-5f)
|
||||
continue;
|
||||
|
||||
uint effectiveGfxObjId = replace is { GfxObjId: not 0u }
|
||||
? replace.GfxObjId
|
||||
: skyObject.GfxObjId;
|
||||
Vector3 anchor = replace is { GfxObjId: not 0u }
|
||||
? replace.AuthoredSortCenter
|
||||
: skyObject.AuthoredSortCenter;
|
||||
if (!IsFiniteDirection(anchor))
|
||||
continue;
|
||||
|
||||
float headingRadians = (replace?.Rotate ?? 0f) * (MathF.PI / 180f);
|
||||
float rotationRadians = skyObject.CurrentAngle(dayFraction)
|
||||
* (MathF.PI / 180f);
|
||||
Matrix4x4 model = Matrix4x4.CreateRotationZ(-headingRadians)
|
||||
* Matrix4x4.CreateRotationY(-rotationRadians);
|
||||
Vector3 transformed = Vector3.TransformNormal(anchor, model);
|
||||
float length = transformed.Length();
|
||||
if (!float.IsFinite(length) || length <= 1e-5f)
|
||||
continue;
|
||||
|
||||
Vector3 direction = transformed / length;
|
||||
if (!IsFiniteDirection(direction) || direction.Z <= 0f)
|
||||
continue;
|
||||
|
||||
source = new AuthoredCelestialShadowSource(
|
||||
kind,
|
||||
index,
|
||||
effectiveGfxObjId,
|
||||
direction,
|
||||
direction.Z,
|
||||
energy);
|
||||
return true;
|
||||
}
|
||||
|
||||
source = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SkyObjectReplaceData? ActiveReplace(
|
||||
DayGroupData dayGroup,
|
||||
float dayFraction,
|
||||
uint objectIndex)
|
||||
{
|
||||
if (dayGroup.SkyTimes.Count == 0)
|
||||
return null;
|
||||
|
||||
DatSkyKeyframeData active = dayGroup.SkyTimes[^1];
|
||||
for (int i = 0; i < dayGroup.SkyTimes.Count; i++)
|
||||
{
|
||||
if (dayGroup.SkyTimes[i].Keyframe.Begin <= dayFraction)
|
||||
active = dayGroup.SkyTimes[i];
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
SkyObjectReplaceData? result = null;
|
||||
foreach (SkyObjectReplaceData replace in active.Replaces)
|
||||
{
|
||||
if (replace.ObjectIndex == objectIndex)
|
||||
result = replace;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsFiniteDirection(Vector3 value) =>
|
||||
float.IsFinite(value.X)
|
||||
&& float.IsFinite(value.Y)
|
||||
&& float.IsFinite(value.Z)
|
||||
&& value.LengthSquared() > 1e-10f;
|
||||
}
|
||||
519
src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs
Normal file
519
src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// The built-in Atmospheric Rendering pack is expressed through the same
|
||||
/// public declaration consumed by third-party packs. Renderer implementation
|
||||
/// code resolves public enum semantics and never recognizes this pack's IDs,
|
||||
/// so the built-in receives no private capability or lifecycle shortcut.
|
||||
/// </summary>
|
||||
internal static class BuiltInAtmosphericRenderPack
|
||||
{
|
||||
internal const string Id = "acdream.atmospheric";
|
||||
|
||||
internal static RenderPackDescriptor Descriptor { get; } = new RenderPackDescriptor(
|
||||
Id,
|
||||
"Atmospheric Rendering",
|
||||
new Version(1, 0, 0),
|
||||
RenderPackApi.Current,
|
||||
RenderPackTier.Tier2Plus,
|
||||
[
|
||||
RenderCapability.MainWorldColorIntermediate,
|
||||
RenderCapability.FullscreenPasses,
|
||||
RenderCapability.SceneDepthSampling,
|
||||
RenderCapability.AuthoredSunDirection,
|
||||
RenderCapability.AuthoredSunScreenPosition,
|
||||
RenderCapability.AuthoredWeather,
|
||||
RenderCapability.DirectionalShadowMaps,
|
||||
RenderCapability.OutdoorDirectionalShadowCasterReplay,
|
||||
RenderCapability.AnimatedCasterTransforms,
|
||||
RenderCapability.AlphaCutoutShadowCasters,
|
||||
RenderCapability.AuthoredCelestialDirectionalLight,
|
||||
],
|
||||
[RenderCapability.GpuTimestampQueries],
|
||||
Resources(),
|
||||
Passes(),
|
||||
SceneReplays(),
|
||||
PipelineVariants(),
|
||||
QualityPresets(),
|
||||
Settings(),
|
||||
AtmospherePolicy())
|
||||
{
|
||||
FeatureSummary = "Filmic HDR atmosphere, moving sun-and-moon shadows from terrain, "
|
||||
+ "trees, buildings, players, and monsters, plus optional volumetric shafts.",
|
||||
};
|
||||
|
||||
internal static IRenderPackAssets CreateAssets(string shaderDirectory) =>
|
||||
new DirectoryRenderPackAssets(shaderDirectory);
|
||||
|
||||
private static IReadOnlyList<RenderResourceDeclaration> Resources() =>
|
||||
[
|
||||
Image("world-hdr", RenderResourceSemantic.MainWorldHdr,
|
||||
RenderFormatClass.HdrColor, 1.0, 1.0, 32L * 1024 * 1024),
|
||||
Image("bloom-a", RenderResourceSemantic.BloomPing,
|
||||
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
|
||||
Image("bloom-b", RenderResourceSemantic.BloomPong,
|
||||
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
|
||||
Image("sun-mask", RenderResourceSemantic.SunOcclusionMask,
|
||||
RenderFormatClass.SingleChannel, 0.25, 0.25, 2L * 1024 * 1024),
|
||||
Image("sun-rays", RenderResourceSemantic.SunRays,
|
||||
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
|
||||
new RenderResourceDeclaration(
|
||||
"directional-shadow-depth",
|
||||
RenderResourceKind.Image2DArray,
|
||||
RenderFormatClass.DirectionalDepth,
|
||||
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, 1024, 1024, Layers: 2),
|
||||
SizeBytes: 0,
|
||||
RenderResourceUsage.Sampled | RenderResourceUsage.DepthAttachment,
|
||||
RenderResourceLifetime.ActivePack,
|
||||
EstimatedResidentBytes: 8L * 1024 * 1024)
|
||||
with { Semantic = RenderResourceSemantic.DirectionalShadowDepth },
|
||||
Image("volumetric", RenderResourceSemantic.VolumetricShafts,
|
||||
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderPassDeclaration> Passes() =>
|
||||
[
|
||||
Pass(
|
||||
"directional-shadow-depth",
|
||||
RenderPassSemantic.DirectionalShadowDepth,
|
||||
RenderPassHook.ShadowDepthBeforeWorld,
|
||||
"directional_shadow_world_opaque.vert.spv",
|
||||
"directional_shadow_world_opaque.frag.spv",
|
||||
[RenderSemanticInput.CameraMatrices,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight,
|
||||
RenderSemanticInput.ShadowCasterTransforms, RenderSemanticInput.ActiveDayGroup,
|
||||
RenderSemanticInput.Weather],
|
||||
[],
|
||||
["directional-shadow-depth"]),
|
||||
Pass(
|
||||
"sun-occlusion",
|
||||
RenderPassSemantic.SunOcclusion,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_sun_occlusion.vert.spv",
|
||||
"atmospheric_sun_occlusion.frag.spv",
|
||||
[RenderSemanticInput.SceneDepth, RenderSemanticInput.SunScreenPosition,
|
||||
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
|
||||
[],
|
||||
["sun-mask"]),
|
||||
Pass(
|
||||
"sun-rays",
|
||||
RenderPassSemantic.SunRays,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_sun_rays.vert.spv",
|
||||
"atmospheric_sun_rays.frag.spv",
|
||||
[RenderSemanticInput.SunScreenPosition, RenderSemanticInput.FrameTime],
|
||||
["sun-mask"],
|
||||
["sun-rays"]),
|
||||
Pass(
|
||||
"volumetric-shafts",
|
||||
RenderPassSemantic.VolumetricShafts,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_volumetric.vert.spv",
|
||||
"atmospheric_volumetric.frag.spv",
|
||||
[RenderSemanticInput.SceneDepth, RenderSemanticInput.CameraMatrices,
|
||||
RenderSemanticInput.SunDirection, RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
|
||||
["directional-shadow-depth"],
|
||||
["volumetric"]),
|
||||
Pass(
|
||||
"bloom-downsample",
|
||||
RenderPassSemantic.BloomDownsample,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_bloom_downsample.vert.spv",
|
||||
"atmospheric_bloom_downsample.frag.spv",
|
||||
[RenderSemanticInput.WorldColor],
|
||||
["sun-rays", "volumetric"],
|
||||
["bloom-a"]),
|
||||
Pass(
|
||||
"bloom-blur-horizontal",
|
||||
RenderPassSemantic.BloomBlurHorizontal,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_bloom_blur.vert.spv",
|
||||
"atmospheric_bloom_blur.frag.spv",
|
||||
[RenderSemanticInput.FrameTime],
|
||||
["bloom-a"],
|
||||
["bloom-b"]),
|
||||
Pass(
|
||||
"bloom-blur-vertical",
|
||||
RenderPassSemantic.BloomBlurVertical,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"atmospheric_bloom_blur.vert.spv",
|
||||
"atmospheric_bloom_blur.frag.spv",
|
||||
[RenderSemanticInput.FrameTime],
|
||||
["bloom-b"],
|
||||
["bloom-a"]),
|
||||
Pass(
|
||||
"filmic-composite",
|
||||
RenderPassSemantic.FilmicComposite,
|
||||
RenderPassHook.ToneMap,
|
||||
"atmospheric_filmic.vert.spv",
|
||||
"atmospheric_filmic.frag.spv",
|
||||
[RenderSemanticInput.WorldColor, RenderSemanticInput.FrameTime],
|
||||
["bloom-a", "sun-rays", "volumetric"],
|
||||
[]),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<SceneReplayDeclaration> SceneReplays() =>
|
||||
[
|
||||
new SceneReplayDeclaration(
|
||||
"outdoor-directional-shadow-casters",
|
||||
RenderSceneReplaySemantic.OutdoorDirectionalShadowCasters,
|
||||
RenderCasterClass.Terrain
|
||||
| RenderCasterClass.OpaqueWorld
|
||||
| RenderCasterClass.AlphaCutoutWorld
|
||||
| RenderCasterClass.AnimatedOpaque
|
||||
| RenderCasterClass.AnimatedAlphaCutout,
|
||||
ViewCount: 4),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<PipelineVariantDeclaration> PipelineVariants() =>
|
||||
[
|
||||
Variant("terrain-shadow-caster", RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.Terrain,
|
||||
"directional_shadow_terrain.vert.spv", "directional_shadow_terrain.frag.spv",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.CameraMatrices]),
|
||||
Variant("world-shadow-opaque", RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"directional_shadow_world_opaque.vert.spv", "directional_shadow_world_opaque.frag.spv",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("world-shadow-cutout", RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"directional_shadow_world_cutout.vert.spv", "directional_shadow_world_cutout.frag.spv",
|
||||
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("terrain-shadow-caster-multiview", RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.Terrain,
|
||||
"directional_shadow_terrain_multiview.vert.spv", "directional_shadow_terrain_multiview.frag.spv",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.CameraMatrices]),
|
||||
Variant("world-shadow-opaque-multiview", RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"directional_shadow_world_opaque_multiview.vert.spv", "directional_shadow_world_opaque_multiview.frag.spv",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("world-shadow-cutout-multiview", RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"directional_shadow_world_cutout_multiview.vert.spv", "directional_shadow_world_cutout_multiview.frag.spv",
|
||||
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("terrain-shadow-receiver", RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.Terrain,
|
||||
"terrain_atmospheric.vert.spv", "terrain_atmospheric.frag.spv",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
Variant("world-shadow-receiver", RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"mesh_atmospheric.vert.spv", "mesh_atmospheric.frag.spv",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AlphaCutout
|
||||
| RenderMaterialClass.AnimatedOpaque | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderQualityPreset> QualityPresets() =>
|
||||
[
|
||||
Preset("low", "Low", RenderQualitySemantic.Low,
|
||||
64, 2.0, 3.0, 0.15, 0.50, 768, 2, 72, 0.25) with
|
||||
{
|
||||
ExecutionHints =
|
||||
RenderQualityExecutionHints.MultiviewDirectionalShadowCascades,
|
||||
},
|
||||
Preset("medium", "Medium", RenderQualitySemantic.Medium,
|
||||
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5),
|
||||
Preset("high", "High", RenderQualitySemantic.High,
|
||||
256, 4.50, 6.00, 0.35, 1.00, 2048, 4, 240, 0.5),
|
||||
Preset("auto", "Auto", RenderQualitySemantic.Automatic,
|
||||
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5)
|
||||
with
|
||||
{
|
||||
SettingOverrides =
|
||||
[
|
||||
new RenderQualitySettingOverride("automatic-quality", "true"),
|
||||
new RenderQualitySettingOverride("volumetric-strength", "0.35"),
|
||||
new RenderQualitySettingOverride("volumetric-ray-steps", "40"),
|
||||
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
|
||||
new RenderQualitySettingOverride("sun-shadow-reach-metres", "144"),
|
||||
new RenderQualitySettingOverride("sun-shadow-pcf-taps", "9"),
|
||||
new RenderQualitySettingOverride("sun-ray-strength", "0.55"),
|
||||
],
|
||||
AutoEligible = false,
|
||||
},
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderSettingDeclaration> Settings() =>
|
||||
[
|
||||
Float("bloom-strength", "Bloom strength", RenderSettingSemantic.BloomStrength,
|
||||
0.65, 0, 2, 0.05),
|
||||
Float("filmic-strength", "Filmic tonemap strength", RenderSettingSemantic.FilmicStrength,
|
||||
1.0, 0, 1, 0.05),
|
||||
Float("exposure", "Exposure", RenderSettingSemantic.Exposure,
|
||||
0.80, 0.25, 4, 0.05),
|
||||
Float("grade-saturation", "Colour saturation", RenderSettingSemantic.GradeSaturation,
|
||||
1.0, 0, 2, 0.05),
|
||||
Float("grade-contrast", "Colour contrast", RenderSettingSemantic.GradeContrast,
|
||||
1.0, 0.5, 2, 0.05),
|
||||
Float("vignette-strength", "Vignette strength", RenderSettingSemantic.VignetteStrength,
|
||||
0.12, 0, 1, 0.01),
|
||||
Float("sun-ray-strength", "Sun-ray strength", RenderSettingSemantic.SunRayStrength,
|
||||
0.55, 0, 2, 0.05),
|
||||
Float("sun-shadow-strength", "Directional-shadow strength",
|
||||
RenderSettingSemantic.DirectionalShadowStrength, 0.72, 0, 1, 0.02),
|
||||
Integer("sun-shadow-reach-metres", "Directional-shadow reach (metres)",
|
||||
RenderSettingSemantic.DirectionalShadowReachMetres, 240, 16, 240, 1),
|
||||
Choice("sun-shadow-pcf-taps", "Directional-shadow filter taps",
|
||||
RenderSettingSemantic.DirectionalShadowPcfTaps, "9", ["1", "9", "25"]),
|
||||
Float("volumetric-strength", "Volumetric-shaft strength",
|
||||
RenderSettingSemantic.VolumetricStrength, 0.35, 0, 1, 0.01),
|
||||
Integer("volumetric-ray-steps", "Volumetric ray-march steps",
|
||||
RenderSettingSemantic.VolumetricRayMarchSteps, 40, 8, 64, 8),
|
||||
new RenderSettingDeclaration(
|
||||
"automatic-quality",
|
||||
"Automatic quality",
|
||||
RenderSettingKind.Boolean,
|
||||
"false",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[])
|
||||
with { Semantic = RenderSettingSemantic.AutomaticQuality },
|
||||
];
|
||||
|
||||
private static AtmospherePolicyDeclaration AtmospherePolicy() => new(
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(-3, 0),
|
||||
new SunElevationResponsePoint(4, 1),
|
||||
new SunElevationResponsePoint(22, 0.75),
|
||||
new SunElevationResponsePoint(55, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
[
|
||||
new ActiveDayGroupMultiplier(0, 1.0),
|
||||
new ActiveDayGroupMultiplier(1, 0.35),
|
||||
new ActiveDayGroupMultiplier(2, 0.20),
|
||||
])
|
||||
{
|
||||
DirectionalShadowLightElevationResponse =
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(1, 0),
|
||||
new SunElevationResponsePoint(12, 1),
|
||||
new SunElevationResponsePoint(90, 1),
|
||||
],
|
||||
VolumetricShaftSunElevationResponse =
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(0, 0),
|
||||
new SunElevationResponsePoint(6, 1),
|
||||
new SunElevationResponsePoint(18, 1),
|
||||
new SunElevationResponsePoint(70, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
};
|
||||
|
||||
private static RenderResourceDeclaration Image(
|
||||
string id,
|
||||
RenderResourceSemantic semantic,
|
||||
RenderFormatClass format,
|
||||
double widthScale,
|
||||
double heightScale,
|
||||
long estimatedBytes) => new RenderResourceDeclaration(
|
||||
id,
|
||||
RenderResourceKind.Image2D,
|
||||
format,
|
||||
new RenderExtentDeclaration(
|
||||
RenderExtentMode.RelativeToMainWorld,
|
||||
widthScale,
|
||||
heightScale),
|
||||
SizeBytes: 0,
|
||||
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
|
||||
RenderResourceLifetime.ActivePack,
|
||||
estimatedBytes)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderPassDeclaration Pass(
|
||||
string id,
|
||||
RenderPassSemantic semantic,
|
||||
RenderPassHook hook,
|
||||
string vertex,
|
||||
string fragment,
|
||||
IReadOnlyList<RenderSemanticInput> semantics,
|
||||
IReadOnlyList<string> reads,
|
||||
IReadOnlyList<string> writes) =>
|
||||
new(id, hook, vertex, fragment, semantics, reads, writes)
|
||||
{
|
||||
Semantic = semantic,
|
||||
};
|
||||
|
||||
private static PipelineVariantDeclaration Variant(
|
||||
string id,
|
||||
RenderPipelineVariantSemantic variantSemantic,
|
||||
RenderPipelineBaseSemantic semantic,
|
||||
string vertex,
|
||||
string fragment,
|
||||
RenderMaterialClass materials,
|
||||
IReadOnlyList<RenderSemanticInput> inputs) =>
|
||||
new(id, semantic, vertex, fragment, materials, inputs)
|
||||
{
|
||||
Semantic = variantSemantic,
|
||||
};
|
||||
|
||||
private static RenderQualityPreset Preset(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderQualitySemantic semantic,
|
||||
long maxMiB,
|
||||
double gpuP50,
|
||||
double gpuP99,
|
||||
double cpuP50,
|
||||
double cpuP99,
|
||||
int shadowResolution,
|
||||
int cascades,
|
||||
int shadowReachMetres,
|
||||
double postScale) => new RenderQualityPreset(
|
||||
id,
|
||||
displayName,
|
||||
semantic == RenderQualitySemantic.Low
|
||||
? [RenderCapability.DirectionalShadowMaps,
|
||||
RenderCapability.MultiviewDirectionalShadowCascades]
|
||||
: [RenderCapability.DirectionalShadowMaps],
|
||||
[
|
||||
Override("directional-shadow-depth", shadowResolution, shadowResolution, cascades,
|
||||
4L * shadowResolution * shadowResolution * cascades),
|
||||
RelativeOverride("bloom-a", postScale),
|
||||
RelativeOverride("bloom-b", postScale),
|
||||
RelativeOverride("sun-mask", id == "low" ? 0.25 : 0.5),
|
||||
RelativeOverride("sun-rays", id == "low" ? 0.25 : 0.5),
|
||||
RelativeOverride("volumetric", id == "high" ? 0.5 : 0.25),
|
||||
],
|
||||
[
|
||||
new RenderQualitySettingOverride("automatic-quality", "false"),
|
||||
new RenderQualitySettingOverride("volumetric-strength", id == "low" ? "0" : "0.35"),
|
||||
new RenderQualitySettingOverride(
|
||||
"volumetric-ray-steps",
|
||||
semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => "24",
|
||||
RenderQualitySemantic.High => "56",
|
||||
_ => "40",
|
||||
}),
|
||||
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
|
||||
new RenderQualitySettingOverride("sun-shadow-reach-metres", shadowReachMetres.ToString()),
|
||||
new RenderQualitySettingOverride(
|
||||
"sun-shadow-pcf-taps",
|
||||
semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => "1",
|
||||
RenderQualitySemantic.High => "25",
|
||||
_ => "9",
|
||||
}),
|
||||
// The renderer recognizes this bounded preset fact; it remains
|
||||
// visible here instead of becoming a hidden cascade constant.
|
||||
new RenderQualitySettingOverride("sun-ray-strength", id == "low" ? "0.4" : "0.55"),
|
||||
],
|
||||
maxMiB * 1024 * 1024,
|
||||
gpuP50,
|
||||
gpuP99,
|
||||
cpuP50,
|
||||
cpuP99)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderQualityResourceOverride Override(
|
||||
string id,
|
||||
int width,
|
||||
int height,
|
||||
int layers,
|
||||
long bytes) => new(
|
||||
id,
|
||||
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, width, height, layers),
|
||||
SizeBytes: 0,
|
||||
EstimatedResidentBytes: bytes);
|
||||
|
||||
private static RenderQualityResourceOverride RelativeOverride(string id, double scale) =>
|
||||
new(
|
||||
id,
|
||||
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, scale, scale),
|
||||
SizeBytes: 0,
|
||||
EstimatedResidentBytes: 0);
|
||||
|
||||
private static RenderSettingDeclaration Float(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
double defaultValue,
|
||||
double min,
|
||||
double max,
|
||||
double step) => new RenderSettingDeclaration(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Float,
|
||||
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
[])
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderSettingDeclaration Integer(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
int defaultValue,
|
||||
int min,
|
||||
int max,
|
||||
int step) => new RenderSettingDeclaration(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Integer,
|
||||
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
[])
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderSettingDeclaration Choice(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
string defaultValue,
|
||||
IReadOnlyList<string> choices) => new RenderSettingDeclaration(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Choice,
|
||||
defaultValue,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
choices)
|
||||
{ Semantic = semantic };
|
||||
}
|
||||
|
||||
internal sealed class DirectoryRenderPackAssets : IRenderPackAssets
|
||||
{
|
||||
private readonly string _root;
|
||||
|
||||
internal DirectoryRenderPackAssets(string root)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(root);
|
||||
_root = Path.GetFullPath(root);
|
||||
}
|
||||
|
||||
public Stream OpenRead(string assetKey)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(assetKey);
|
||||
string normalized = assetKey.Replace('/', Path.DirectorySeparatorChar);
|
||||
string path = Path.GetFullPath(Path.Combine(_root, normalized));
|
||||
string relative = Path.GetRelativePath(_root, path);
|
||||
if (Path.IsPathRooted(relative)
|
||||
|| relative == ".."
|
||||
|| relative.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
|
||||
throw new UnauthorizedAccessException("The asset key escapes the render-pack root.");
|
||||
return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,965 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// API-v1 executor for declaration-only fullscreen graphs. It supports the
|
||||
/// portable Tier-1 hooks/resources without recognizing a pack id or shader
|
||||
/// filename. Scene replay and renderer-pipeline variants remain separate host
|
||||
/// facilities and are rejected by the factory before this runtime is built.
|
||||
/// </summary>
|
||||
internal class DeclaredFullscreenRenderPackGraph :
|
||||
IAtmosphericWorldGraphRuntime,
|
||||
IRenderPackRuntimePerformanceSource,
|
||||
IRenderPackRuntimeDiagnosticsSource
|
||||
{
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly IDisposable _hdrLease;
|
||||
private readonly IGpuSampler _sampler;
|
||||
private readonly Node[] _nodes;
|
||||
private readonly IReadOnlyDictionary<string, RenderResourceDeclaration> _resources;
|
||||
private readonly PackSettingsUniforms _settings;
|
||||
private readonly DirectionalSunShadowRenderer? _directionalShadows;
|
||||
private readonly DirectionalShadowCasterFrame _shadowCasters = new();
|
||||
private readonly RenderPassDeclaration? _shadowPass;
|
||||
private readonly float _shadowStrength;
|
||||
private TargetSet? _targets;
|
||||
private RenderPackResourceBudget _resourceBudget;
|
||||
private long _resourceGeneration;
|
||||
private long _residentGpuBudgetBytes;
|
||||
private AtmosphericFrameInputs _lastInputs;
|
||||
private DirectionalSunShadowDiagnostics _lastShadowDiagnostics;
|
||||
private int _lastShadowCasterCount;
|
||||
private int _lastShadowClassificationCalls;
|
||||
private WbDrawDispatcher? _lastShadowWorldMeshes;
|
||||
private long _lastShadowFrameSerial = -1;
|
||||
private bool _renderedFrame;
|
||||
private bool _disposed;
|
||||
|
||||
internal DeclaredFullscreenRenderPackGraph(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
: this(
|
||||
device,
|
||||
descriptor,
|
||||
RenderPackShaderAssets.Validate(descriptor, assets),
|
||||
preset,
|
||||
userSettingOverrides)
|
||||
{
|
||||
}
|
||||
|
||||
internal DeclaredFullscreenRenderPackGraph(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
Preset = preset ?? throw new ArgumentNullException(nameof(preset));
|
||||
ArgumentNullException.ThrowIfNull(userSettingOverrides);
|
||||
if (device is not IGpuPipelineFormatVariantHost variants)
|
||||
throw new NotSupportedException("The active RHI cannot build an HDR world intermediate.");
|
||||
|
||||
_resources = descriptor.Resources.ToDictionary(value => value.Id, StringComparer.OrdinalIgnoreCase);
|
||||
RenderPassDeclaration[] passes = descriptor.Passes
|
||||
.OrderBy(value => value.Hook)
|
||||
.ToArray();
|
||||
RenderPassDeclaration[] fullscreenPasses = passes
|
||||
.Where(static value =>
|
||||
value.Semantic != RenderPassSemantic.DirectionalShadowDepth)
|
||||
.ToArray();
|
||||
if (!fullscreenPasses.Any(value => value.Hook == RenderPassHook.ToneMap
|
||||
&& value.ResourceWrites.Count == 0))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Fullscreen pack '{descriptor.Id}' must declare a ToneMap pass that writes the output surface.");
|
||||
}
|
||||
|
||||
IDisposable? lease = null;
|
||||
DirectionalSunShadowRenderer? directionalShadows = null;
|
||||
var nodes = new List<Node>(fullscreenPasses.Length);
|
||||
try
|
||||
{
|
||||
lease = variants.AcquirePipelineColorFormat(GpuTextureFormat.Rgba16FloatRenderTarget);
|
||||
_sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
||||
foreach (RenderPassDeclaration pass in fullscreenPasses)
|
||||
{
|
||||
if (pass.Hook is not RenderPassHook.AtmosphereBeforeToneMap
|
||||
and not RenderPassHook.ToneMap)
|
||||
throw new NotSupportedException($"Fullscreen executor does not support hook '{pass.Hook}'.");
|
||||
RenderSemanticInput? unsupported = pass.SemanticInputs.FirstOrDefault(value =>
|
||||
value is RenderSemanticInput.SceneNormals
|
||||
or RenderSemanticInput.ShadowCasterTransforms
|
||||
or RenderSemanticInput.DirectionalShadowMaps);
|
||||
if (unsupported is RenderSemanticInput.SceneNormals
|
||||
or RenderSemanticInput.ShadowCasterTransforms
|
||||
or RenderSemanticInput.DirectionalShadowMaps)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Tier-1 fullscreen pass '{pass.Id}' requires unsupported semantic '{unsupported}'.");
|
||||
}
|
||||
if (pass.ResourceWrites.Count > 1)
|
||||
throw new NotSupportedException($"Pass '{pass.Id}' writes more than one colour target.");
|
||||
GpuTextureFormat format = pass.ResourceWrites.Count == 0
|
||||
? GpuTextureFormat.Rgba8UnormRenderTarget
|
||||
: ValidateOutput(Resource(pass.ResourceWrites[0]));
|
||||
var pipeline = device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = $"render-pack-{descriptor.Id}-{pass.Id}",
|
||||
Shaders = RenderPackShaderAssets.LoadPass(descriptor, assets, pass),
|
||||
VertexLayout = GpuVertexLayout.None,
|
||||
Blend = GpuBlendMode.None,
|
||||
Depth = GpuDepthState.Disabled,
|
||||
Cull = GpuCullMode.None,
|
||||
ColorFormat = format,
|
||||
AllowColorFormatVariants = false,
|
||||
SampleCount = 1,
|
||||
UsesRenderPackShaderAbi = true,
|
||||
});
|
||||
string timerName = $"render-pack-{descriptor.Id}-{pass.Id}";
|
||||
nodes.Add(new Node(
|
||||
pass,
|
||||
pipeline,
|
||||
[.. RenderPackTextureBindingResolver.Resolve(pass, _resources)],
|
||||
timerName));
|
||||
}
|
||||
_nodes = [.. nodes];
|
||||
_settings = PackSettingsUniforms.Create(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
_shadowPass = passes.SingleOrDefault(static value =>
|
||||
value.Semantic == RenderPassSemantic.DirectionalShadowDepth);
|
||||
_shadowStrength = _shadowPass is null
|
||||
? 0f
|
||||
: ReadSemanticSetting(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides,
|
||||
RenderSettingSemantic.DirectionalShadowStrength);
|
||||
if (_shadowPass is not null)
|
||||
{
|
||||
directionalShadows = new DirectionalSunShadowRenderer(
|
||||
device,
|
||||
ResolveShadowQuality(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides),
|
||||
RenderPackAtmospherePolicyEvaluation.NeutralDirectionalShadowElevation,
|
||||
LoadDirectionalShadowShaders(descriptor, assets),
|
||||
multiviewCascades: (preset.ExecutionHints
|
||||
& RenderQualityExecutionHints
|
||||
.MultiviewDirectionalShadowCascades) != 0);
|
||||
}
|
||||
_directionalShadows = directionalShadows;
|
||||
directionalShadows = null;
|
||||
_hdrLease = lease;
|
||||
lease = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
directionalShadows?.Dispose();
|
||||
for (int i = nodes.Count - 1; i >= 0; i--)
|
||||
nodes[i].Pipeline.Dispose();
|
||||
lease?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public RenderPackDescriptor Descriptor { get; }
|
||||
|
||||
public RenderQualityPreset Preset { get; }
|
||||
|
||||
internal IDirectionalShadowReceiverSource DeclaredDirectionalShadowReceivers =>
|
||||
_directionalShadows
|
||||
?? throw new InvalidOperationException(
|
||||
$"Pack '{Descriptor.Id}' has no declared directional-shadow executor.");
|
||||
|
||||
internal DirectionalSunShadowDiagnostics RenderDeclaredDirectionalShadows(
|
||||
IGpuFrame frame,
|
||||
in RenderFrameFoundation foundation,
|
||||
in WorldRenderFrame world,
|
||||
int activeDayGroup,
|
||||
in RenderSceneQuery scene,
|
||||
WbDrawDispatcher worldMeshes,
|
||||
TerrainModernRenderer terrain)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
DirectionalSunShadowRenderer renderer = _directionalShadows
|
||||
?? throw new InvalidOperationException(
|
||||
$"Pack '{Descriptor.Id}' has no declared directional-shadow executor.");
|
||||
_shadowCasters.Build(in scene);
|
||||
AuthoredCelestialShadowSource source = world.CelestialShadowSource;
|
||||
float elevationStrength = RenderPackAtmospherePolicyEvaluation
|
||||
.DirectionalShadowFromSin(
|
||||
Descriptor.AtmospherePolicy!.DirectionalShadowLightElevationResponse,
|
||||
source.ElevationSin,
|
||||
fallback: 0f);
|
||||
var environment = new DirectionalShadowEnvironmentInput(
|
||||
PackEnabled: true,
|
||||
PortalOrLoginCoverVisible: foundation.PortalViewportVisible,
|
||||
PlayerInsideCell: world.Roots.PlayerInsideCell
|
||||
|| world.Roots.CameraInsideCell,
|
||||
source,
|
||||
foundation.Atmosphere,
|
||||
ActiveDayGroupMultiplier: Math.Clamp(
|
||||
EvaluateDayGroupPolicy(activeDayGroup)
|
||||
* elevationStrength
|
||||
* _shadowStrength,
|
||||
0f,
|
||||
1f));
|
||||
var input = new DirectionalSunShadowRenderInput(
|
||||
environment,
|
||||
world.Camera.Camera.View,
|
||||
world.Camera.Projection,
|
||||
_shadowCasters,
|
||||
ResidentMaximumReachMeters:
|
||||
world.ResidentStreamingWindow.MaximumReachMeters);
|
||||
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
|
||||
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
||||
_lastShadowDiagnostics = renderer.Render(
|
||||
frame,
|
||||
in input,
|
||||
worldMeshes,
|
||||
terrain);
|
||||
_lastShadowWorldMeshes = worldMeshes;
|
||||
_lastShadowFrameSerial = frame.Serial;
|
||||
RequireRetainedGpuBudget(renderer);
|
||||
return _lastShadowDiagnostics;
|
||||
}
|
||||
|
||||
public IGpuRenderTarget PrepareWorldTarget(int width, int height, int sampleCount)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_targets is { } current
|
||||
&& current.Width == width
|
||||
&& current.Height == height
|
||||
&& current.SampleCount == sampleCount)
|
||||
return current.World;
|
||||
RenderPackHostCapabilities capabilities =
|
||||
RenderPackCapabilityResolver.Resolve(_device.Capabilities);
|
||||
RenderPackResourceBudget budget = RenderPackResourceBudgetPlanner.RequireWithinHost(
|
||||
Descriptor,
|
||||
Preset,
|
||||
width,
|
||||
height,
|
||||
sampleCount,
|
||||
capabilities);
|
||||
TargetSet candidate = TargetSet.Create(
|
||||
_device,
|
||||
Descriptor,
|
||||
Preset,
|
||||
_sampler,
|
||||
width,
|
||||
height,
|
||||
sampleCount);
|
||||
TargetSet? prior = _targets;
|
||||
_targets = candidate;
|
||||
_resourceBudget = budget;
|
||||
_residentGpuBudgetBytes = Math.Min(
|
||||
Preset.MaxResidentGpuBytes,
|
||||
capabilities.MaxPackResidentBytes);
|
||||
_resourceGeneration = checked(_resourceGeneration + 1);
|
||||
_renderedFrame = false;
|
||||
prior?.Dispose();
|
||||
return candidate.World;
|
||||
}
|
||||
|
||||
public void RenderPostProcess(IGpuFrame frame, in AtmosphericFrameInputs inputs)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
TargetSet targets = _targets
|
||||
?? throw new InvalidOperationException("PrepareWorldTarget must run before the fullscreen graph.");
|
||||
if (inputs.ViewportWidth != targets.Width || inputs.ViewportHeight != targets.Height)
|
||||
throw new InvalidOperationException("Fullscreen graph inputs and targets belong to different frames.");
|
||||
|
||||
float elevationPolicy = EvaluateSunElevationPolicy(inputs.SunElevationDegrees);
|
||||
float dayGroupPolicy = EvaluateDayGroupPolicy(inputs.ActiveDayGroup);
|
||||
IReadOnlyList<SunElevationResponsePoint> shadowCurve =
|
||||
Descriptor.AtmospherePolicy?.DirectionalShadowLightElevationResponse ?? [];
|
||||
IReadOnlyList<SunElevationResponsePoint> volumetricCurve =
|
||||
Descriptor.AtmospherePolicy?.VolumetricShaftSunElevationResponse ?? [];
|
||||
float shadowElevationPolicy = shadowCurve.Count == 0
|
||||
? elevationPolicy
|
||||
: RenderPackAtmospherePolicyEvaluation.DirectionalShadow(
|
||||
shadowCurve,
|
||||
inputs.SunElevationDegrees,
|
||||
elevationPolicy);
|
||||
float volumetricElevationPolicy = volumetricCurve.Count == 0
|
||||
? 0f
|
||||
: RenderPackAtmospherePolicyEvaluation.VolumetricShaft(
|
||||
volumetricCurve,
|
||||
inputs.SunElevationDegrees,
|
||||
0f);
|
||||
float sunPolicy = EvaluateSunPolicy(
|
||||
inputs,
|
||||
elevationPolicy,
|
||||
dayGroupPolicy);
|
||||
var frameValues = new AtmosphericFrameUniforms(
|
||||
new Vector4(inputs.SunScreenUv, sunPolicy, inputs.SunElevationDegrees),
|
||||
new Vector4(inputs.SunColor, sunPolicy),
|
||||
new Vector4(targets.Width, targets.Height, 1f / targets.Width, 1f / targets.Height),
|
||||
new Vector4((float)inputs.Weather, inputs.WeatherIntensity,
|
||||
(float)Math.Clamp(inputs.DeltaSeconds, 0d, 1d), inputs.IsOutdoor ? 1f : 0f),
|
||||
new Vector4(inputs.SunDirection, inputs.SunDirectionalBrightness),
|
||||
new Vector4(
|
||||
inputs.ActiveDayGroup,
|
||||
dayGroupPolicy,
|
||||
shadowElevationPolicy,
|
||||
volumetricElevationPolicy),
|
||||
inputs.InverseViewProjection);
|
||||
GpuRingAllocation frameBlock = frame.AllocateRing(AtmosphericFrameUniforms.SizeInBytes, GpuRingUsage.Uniform);
|
||||
MemoryMarshal.Write(frameBlock.Data, in frameValues);
|
||||
GpuRingAllocation settingsBlock = frame.AllocateRing(PackSettingsUniforms.SizeInBytes, GpuRingUsage.Uniform);
|
||||
PackSettingsUniforms settings = _settings;
|
||||
MemoryMarshal.Write(settingsBlock.Data, in settings);
|
||||
|
||||
foreach (Node node in _nodes)
|
||||
Draw(frame, node, targets, frameBlock, settingsBlock);
|
||||
_lastInputs = inputs;
|
||||
_renderedFrame = true;
|
||||
}
|
||||
|
||||
public RenderPackRuntimeDiagnostics CaptureDiagnostics()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
TargetSet? targets = _targets;
|
||||
if (!_renderedFrame || targets is null)
|
||||
return RenderPackRuntimeDiagnostics.Empty(Preset.Id);
|
||||
|
||||
int shadowPassCount = _shadowPass is null ? 0 : 1;
|
||||
var passes = new RenderPackPassDiagnostics[_nodes.Length + shadowPassCount];
|
||||
int passIndex = 0;
|
||||
if (_shadowPass is not null)
|
||||
{
|
||||
passes[passIndex++] = new RenderPackPassDiagnostics(
|
||||
_shadowPass.Id,
|
||||
_lastShadowDiagnostics.LastResolvedGpuMilliseconds,
|
||||
_lastShadowDiagnostics.DrawCalls,
|
||||
DispatchCalls: 0);
|
||||
}
|
||||
for (int i = 0; i < _nodes.Length; i++)
|
||||
{
|
||||
Node node = _nodes[i];
|
||||
_device.Timers.TryResolve(node.TimerName, out double milliseconds);
|
||||
passes[passIndex++] = new RenderPackPassDiagnostics(
|
||||
node.Pass.Id,
|
||||
milliseconds,
|
||||
DrawCalls: 1,
|
||||
DispatchCalls: 0);
|
||||
}
|
||||
|
||||
return new RenderPackRuntimeDiagnostics(
|
||||
Preset.Id,
|
||||
checked(
|
||||
_resourceBudget.RetainedGpuBytes
|
||||
+ (_directionalShadows?.RetainedGpuBufferBytes ?? 0L)),
|
||||
_resourceBudget.MultisampleGpuBytes,
|
||||
targets.ImageCount + shadowPassCount,
|
||||
BufferCount: _directionalShadows?.RetainedGpuBufferCount ?? 0,
|
||||
DrawCalls: _nodes.Length + _lastShadowDiagnostics.DrawCalls,
|
||||
DispatchCalls: 0,
|
||||
ShadowCasterCount: _lastShadowCasterCount,
|
||||
CascadeDrawCount: _lastShadowDiagnostics.CascadeCount,
|
||||
CpuClassificationCalls: _lastShadowClassificationCalls,
|
||||
_lastInputs.SunElevationDegrees,
|
||||
_lastInputs.ActiveDayGroup,
|
||||
_lastInputs.Weather.ToString(),
|
||||
_lastInputs.WeatherIntensity,
|
||||
_lastInputs.IsOutdoor,
|
||||
DirectionalShadowStrength: _lastShadowDiagnostics.Strength,
|
||||
passes)
|
||||
{
|
||||
DirectionalShadowSourceKind = _lastShadowDiagnostics.SourceKind,
|
||||
DirectionalShadowSourceObjectIndex =
|
||||
_lastShadowDiagnostics.SourceObjectIndex,
|
||||
DirectionalShadowSourceGfxObjId =
|
||||
_lastShadowDiagnostics.SourceGfxObjId,
|
||||
DirectionalShadowSurfaceToLightDirection =
|
||||
_lastShadowDiagnostics.SurfaceToLightDirection,
|
||||
DirectionalShadowLightElevationSin =
|
||||
_lastShadowDiagnostics.LightElevationSin,
|
||||
ShadowTransformChurn = _lastShadowDiagnostics.TransformChurn,
|
||||
SharedWorldTransformUsedInstances =
|
||||
_lastShadowWorldMeshes is not null
|
||||
&& _lastShadowWorldMeshes.HasDirectionalShadowTransformFrame(
|
||||
_lastShadowFrameSerial)
|
||||
? _lastShadowWorldMeshes
|
||||
.DirectionalShadowTransformFrameUsedInstances
|
||||
: 0u,
|
||||
};
|
||||
}
|
||||
|
||||
public RenderPackRuntimePerformanceMetrics CapturePerformanceMetrics()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
double gpuMilliseconds = 0d;
|
||||
bool resolved = _targets is not null;
|
||||
for (int i = 0; i < _nodes.Length; i++)
|
||||
{
|
||||
if (!_device.Timers.TryTakeResolved(
|
||||
_nodes[i].TimerName,
|
||||
out double milliseconds))
|
||||
{
|
||||
resolved = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
gpuMilliseconds += milliseconds;
|
||||
}
|
||||
}
|
||||
if (_directionalShadows is not null)
|
||||
{
|
||||
if (!_device.Timers.TryTakeResolved(
|
||||
RenderPackPerformanceScopeNames.EnhancedWorldReceiver,
|
||||
out double receiverMilliseconds))
|
||||
{
|
||||
resolved = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
gpuMilliseconds += receiverMilliseconds;
|
||||
}
|
||||
int shadowTimerCount = _directionalShadows.MultiviewCascadesEnabled
|
||||
&& _lastShadowDiagnostics.CascadeCount > 0
|
||||
? 1
|
||||
: _lastShadowDiagnostics.CascadeCount;
|
||||
for (int i = 0; i < shadowTimerCount; i++)
|
||||
{
|
||||
if (!_device.Timers.TryTakeResolved(
|
||||
_directionalShadows.MultiviewCascadesEnabled
|
||||
? DirectionalSunShadowRenderer.MultiviewTimerName
|
||||
: DirectionalSunShadowRenderer.TimerName(i),
|
||||
out double milliseconds))
|
||||
{
|
||||
resolved = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
gpuMilliseconds += milliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new RenderPackRuntimePerformanceMetrics(
|
||||
_resourceGeneration,
|
||||
resolved,
|
||||
resolved ? gpuMilliseconds : 0d,
|
||||
checked(
|
||||
_resourceBudget.RetainedGpuBytes
|
||||
+ (_directionalShadows?.RetainedGpuBufferBytes ?? 0L)),
|
||||
_resourceBudget.MultisampleGpuBytes);
|
||||
}
|
||||
|
||||
private void RequireRetainedGpuBudget(
|
||||
DirectionalSunShadowRenderer renderer)
|
||||
{
|
||||
long total = checked(
|
||||
_resourceBudget.RetainedGpuBytes
|
||||
+ renderer.RetainedGpuBufferBytes);
|
||||
if (total <= _residentGpuBudgetBytes)
|
||||
return;
|
||||
throw new NotSupportedException(
|
||||
$"Render pack preset '{Preset.Id}' needs {total} resident GPU bytes "
|
||||
+ "after materializing its scene-dependent shadow command buffers; "
|
||||
+ $"the active pack budget is {_residentGpuBudgetBytes} bytes.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_targets?.Dispose();
|
||||
_directionalShadows?.Dispose();
|
||||
for (int i = _nodes.Length - 1; i >= 0; i--)
|
||||
_nodes[i].Pipeline.Dispose();
|
||||
_hdrLease.Dispose();
|
||||
}
|
||||
|
||||
private void Draw(
|
||||
IGpuFrame frame,
|
||||
Node node,
|
||||
TargetSet targets,
|
||||
GpuRingAllocation frameBlock,
|
||||
GpuRingAllocation settingsBlock)
|
||||
{
|
||||
IGpuRenderTarget? output = node.Pass.ResourceWrites.Count == 0
|
||||
? null
|
||||
: targets.Resource(node.Pass.ResourceWrites[0]).Target;
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
Name = node.TimerName,
|
||||
Color = new GpuColorAttachment(output, GpuLoadOp.Clear, GpuStoreOp.Store, Vector4.Zero),
|
||||
Depth = null,
|
||||
SampleCount = 1,
|
||||
});
|
||||
using IDisposable timer = encoder.BeginTimerScope(node.TimerName);
|
||||
encoder.BindPipeline(node.Pipeline);
|
||||
encoder.BindUniformBuffer(GpuBindingModel.UniformAtmosphericFrame,
|
||||
frameBlock.Buffer, frameBlock.OffsetBytes, AtmosphericFrameUniforms.SizeInBytes);
|
||||
GpuRingAllocation passBlock = frame.AllocateRing(
|
||||
AtmosphericPackPassUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
var zero = AtmosphericPackPassUniforms.From(Vector4.Zero);
|
||||
MemoryMarshal.Write(passBlock.Data, in zero);
|
||||
encoder.BindUniformBuffer(GpuBindingModel.UniformPackPass,
|
||||
passBlock.Buffer, passBlock.OffsetBytes, AtmosphericPackPassUniforms.SizeInBytes);
|
||||
encoder.BindUniformBuffer(GpuBindingModel.UniformPackSettings,
|
||||
settingsBlock.Buffer, settingsBlock.OffsetBytes, PackSettingsUniforms.SizeInBytes);
|
||||
|
||||
Span<GpuTextureSlot> slots = stackalloc GpuTextureSlot[4];
|
||||
slots.Fill(GpuTextureSlot.Unassigned);
|
||||
for (int i = 0; i < node.Inputs.Length; i++)
|
||||
slots[i] = Resolve(node.Inputs[i], targets);
|
||||
GpuPushConstants push = GpuPushConstants.Default;
|
||||
push.TextureIndexA = slots[0].Index;
|
||||
push.TextureIndexB = slots[1].Index;
|
||||
push.ParamA = BitConverter.UInt32BitsToSingle(slots[2].Index);
|
||||
push.ParamB = BitConverter.UInt32BitsToSingle(slots[3].Index);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.Draw(3, 1, 0, 0);
|
||||
}
|
||||
|
||||
private static GpuTextureSlot Resolve(RenderPackTextureInput input, TargetSet targets)
|
||||
{
|
||||
if (input.Semantic is { } semantic)
|
||||
{
|
||||
return semantic switch
|
||||
{
|
||||
RenderSemanticInput.WorldColor => targets.WorldColor,
|
||||
RenderSemanticInput.SceneDepth => targets.WorldDepth,
|
||||
_ => throw new NotSupportedException($"Texture semantic '{semantic}' is unsupported by Tier-1."),
|
||||
};
|
||||
}
|
||||
return targets.Resource(input.ResourceId!).Slot;
|
||||
}
|
||||
|
||||
private float EvaluateSunElevationPolicy(float elevation)
|
||||
{
|
||||
IReadOnlyList<SunElevationResponsePoint>? points =
|
||||
Descriptor.AtmospherePolicy?.SunElevationResponse;
|
||||
return RenderPackAtmospherePolicyEvaluation.Ray(points, elevation);
|
||||
}
|
||||
|
||||
private float EvaluateDayGroupPolicy(int activeDayGroup)
|
||||
{
|
||||
ActiveDayGroupMultiplier? value = Descriptor.AtmospherePolicy?
|
||||
.ActiveDayGroupMultipliers
|
||||
.FirstOrDefault(entry => entry.ActiveDayGroup == activeDayGroup);
|
||||
return value is null ? 1f : (float)value.Multiplier;
|
||||
}
|
||||
|
||||
private static float EvaluateSunPolicy(
|
||||
in AtmosphericFrameInputs inputs,
|
||||
float elevationPolicy,
|
||||
float dayGroupPolicy)
|
||||
{
|
||||
if (!inputs.IsOutdoor || !inputs.SunIsOnScreen)
|
||||
return 0f;
|
||||
return Math.Clamp(
|
||||
elevationPolicy
|
||||
* dayGroupPolicy
|
||||
* EvaluateWeatherPolicy(inputs.Weather, inputs.WeatherIntensity),
|
||||
0f,
|
||||
4f);
|
||||
}
|
||||
|
||||
private static float EvaluateWeatherPolicy(
|
||||
AcDream.Core.World.WeatherKind weather,
|
||||
float intensity)
|
||||
{
|
||||
float weatherTarget = weather switch
|
||||
{
|
||||
AcDream.Core.World.WeatherKind.Clear => 1f,
|
||||
AcDream.Core.World.WeatherKind.Overcast => 0.18f,
|
||||
AcDream.Core.World.WeatherKind.Rain => 0.10f,
|
||||
AcDream.Core.World.WeatherKind.Snow => 0.16f,
|
||||
AcDream.Core.World.WeatherKind.Storm => 0.06f,
|
||||
_ => 0f,
|
||||
};
|
||||
return 1f + ((weatherTarget - 1f) * Math.Clamp(intensity, 0f, 1f));
|
||||
}
|
||||
|
||||
private static DirectionalShadowPipelineShaders LoadDirectionalShadowShaders(
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets)
|
||||
{
|
||||
DirectionalShadowPipelineShaders shaders = new(
|
||||
Variant(RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster),
|
||||
Variant(RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster),
|
||||
Variant(RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster),
|
||||
Variant(RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver),
|
||||
Variant(RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver));
|
||||
if (descriptor.PipelineVariants.Any(value =>
|
||||
value.Semantic == RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster))
|
||||
{
|
||||
shaders = shaders with
|
||||
{
|
||||
MultiviewCasters = new DirectionalShadowMultiviewPipelineShaders(
|
||||
Variant(RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster),
|
||||
Variant(RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster),
|
||||
Variant(RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster)),
|
||||
};
|
||||
}
|
||||
return shaders;
|
||||
|
||||
GpuShaderSet Variant(RenderPipelineVariantSemantic semantic)
|
||||
{
|
||||
PipelineVariantDeclaration variant = descriptor.PipelineVariants
|
||||
.Single(value => value.Semantic == semantic);
|
||||
return RenderPackShaderAssets.LoadVariant(descriptor, assets, variant);
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectionalShadowQuality ResolveShadowQuality(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
{
|
||||
DirectionalShadowPreset shadowPreset = preset.Semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => DirectionalShadowPreset.Low,
|
||||
RenderQualitySemantic.High => DirectionalShadowPreset.High,
|
||||
_ => DirectionalShadowPreset.Medium,
|
||||
};
|
||||
DirectionalShadowQuality quality = DirectionalShadowQuality.For(shadowPreset);
|
||||
RenderResourceDeclaration resource = descriptor.Resources.Single(value =>
|
||||
value.Semantic == RenderResourceSemantic.DirectionalShadowDepth);
|
||||
RenderExtentDeclaration extent = preset.ResourceOverrides.FirstOrDefault(value =>
|
||||
string.Equals(
|
||||
value.ResourceId,
|
||||
resource.Id,
|
||||
StringComparison.OrdinalIgnoreCase))?.Extent
|
||||
?? resource.Extent
|
||||
?? throw new NotSupportedException(
|
||||
"The DirectionalShadowDepth semantic resource has no image extent.");
|
||||
if (extent.Mode != RenderExtentMode.AbsolutePixels
|
||||
|| extent.Width != extent.Height
|
||||
|| extent.Width != Math.Truncate(extent.Width)
|
||||
|| extent.Width is < 1 or > 16_384
|
||||
|| extent.Layers is < 1 or > 4)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The DirectionalShadowDepth semantic resource must be a square "
|
||||
+ "absolute 1..16384 image with 1..4 array layers.");
|
||||
}
|
||||
|
||||
float reach = ReadSemanticSetting(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides,
|
||||
RenderSettingSemantic.DirectionalShadowReachMetres);
|
||||
int taps = ReadShadowPcfTaps(descriptor, preset, userSettingOverrides);
|
||||
int radius = taps switch
|
||||
{
|
||||
1 => 0,
|
||||
9 => 1,
|
||||
25 => 2,
|
||||
_ => throw new NotSupportedException(
|
||||
"DirectionalShadowPcfTaps must resolve to exactly 1, 9, or 25 samples."),
|
||||
};
|
||||
int resolution = checked((int)extent.Width);
|
||||
int cascades = extent.Layers;
|
||||
return quality with
|
||||
{
|
||||
CascadeCount = cascades,
|
||||
MapResolution = resolution,
|
||||
MaximumReachMeters = Math.Clamp(reach, 1f, 10_000f),
|
||||
PcfRadiusTexels = radius,
|
||||
ApproximateDepthMapBytes = checked(
|
||||
(long)cascades * resolution * resolution * sizeof(float)),
|
||||
IncrementalGpuP50BudgetMilliseconds = preset.MaxIncrementalGpuMillisecondsP50,
|
||||
IncrementalGpuP99BudgetMilliseconds = preset.MaxIncrementalGpuMillisecondsP99,
|
||||
IncrementalCpuP50BudgetMilliseconds = preset.MaxIncrementalCpuMillisecondsP50,
|
||||
IncrementalCpuP99BudgetMilliseconds = preset.MaxIncrementalCpuMillisecondsP99,
|
||||
PackResidentGpuByteBudget = preset.MaxResidentGpuBytes,
|
||||
};
|
||||
}
|
||||
|
||||
private static int ReadShadowPcfTaps(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
{
|
||||
RenderSettingDeclaration setting = descriptor.Settings.Single(value =>
|
||||
value.Semantic == RenderSettingSemantic.DirectionalShadowPcfTaps);
|
||||
string value = RenderPackSettingResolution.Resolve(
|
||||
setting,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
return int.TryParse(
|
||||
value,
|
||||
System.Globalization.NumberStyles.Integer,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out int taps)
|
||||
? taps
|
||||
: throw new NotSupportedException(
|
||||
"DirectionalShadowPcfTaps must resolve to an integer sample count.");
|
||||
}
|
||||
|
||||
private static float ReadSemanticSetting(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides,
|
||||
RenderSettingSemantic semantic)
|
||||
{
|
||||
RenderSettingDeclaration setting = descriptor.Settings.Single(value =>
|
||||
value.Semantic == semantic);
|
||||
string value = RenderPackSettingResolution.Resolve(
|
||||
setting,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
if (!RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
|
||||
|| !float.IsFinite(encoded))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Setting semantic '{semantic}' did not resolve to a finite value.");
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
private RenderResourceDeclaration Resource(string id) =>
|
||||
_resources.TryGetValue(id, out RenderResourceDeclaration? value)
|
||||
? value
|
||||
: throw new InvalidOperationException($"Unknown render-pack resource '{id}'.");
|
||||
|
||||
private static GpuTextureFormat FormatOf(RenderResourceDeclaration resource) => resource.Format switch
|
||||
{
|
||||
RenderFormatClass.HdrColor => GpuTextureFormat.Rgba16FloatRenderTarget,
|
||||
RenderFormatClass.LdrColor or RenderFormatClass.SingleChannel =>
|
||||
GpuTextureFormat.Rgba8UnormRenderTarget,
|
||||
_ => throw new NotSupportedException(
|
||||
$"Fullscreen resource '{resource.Id}' has unsupported format '{resource.Format}'."),
|
||||
};
|
||||
|
||||
private static GpuTextureFormat ValidateOutput(RenderResourceDeclaration resource)
|
||||
{
|
||||
if (resource.Kind != RenderResourceKind.Image2D
|
||||
|| (resource.Usage & RenderResourceUsage.ColorAttachment) == 0
|
||||
|| resource.Extent is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Fullscreen output '{resource.Id}' must be an extent-declared colour Image2D.");
|
||||
}
|
||||
return FormatOf(resource);
|
||||
}
|
||||
|
||||
private sealed record Node(
|
||||
RenderPassDeclaration Pass,
|
||||
IGpuPipeline Pipeline,
|
||||
RenderPackTextureInput[] Inputs,
|
||||
string TimerName);
|
||||
|
||||
private sealed class TargetSet : IDisposable
|
||||
{
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly Dictionary<string, ResourceTarget> _resources;
|
||||
private readonly GpuTextureSlot[] _slots;
|
||||
private readonly string? _mainWorldResourceId;
|
||||
|
||||
private TargetSet(
|
||||
IGpuDevice device,
|
||||
int width,
|
||||
int height,
|
||||
int sampleCount,
|
||||
IGpuRenderTarget world,
|
||||
GpuTextureSlot worldColor,
|
||||
GpuTextureSlot worldDepth,
|
||||
Dictionary<string, ResourceTarget> resources,
|
||||
GpuTextureSlot[] slots,
|
||||
string? mainWorldResourceId)
|
||||
{
|
||||
_device = device;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SampleCount = sampleCount;
|
||||
World = world;
|
||||
WorldColor = worldColor;
|
||||
WorldDepth = worldDepth;
|
||||
_resources = resources;
|
||||
_slots = slots;
|
||||
_mainWorldResourceId = mainWorldResourceId;
|
||||
}
|
||||
|
||||
internal int Width { get; }
|
||||
internal int Height { get; }
|
||||
internal int SampleCount { get; }
|
||||
internal IGpuRenderTarget World { get; }
|
||||
internal GpuTextureSlot WorldColor { get; }
|
||||
internal GpuTextureSlot WorldDepth { get; }
|
||||
internal int ImageCount => checked(
|
||||
2
|
||||
+ _resources.Count
|
||||
+ (SampleCount > 1 ? (WorldDepth.IsAssigned ? 2 : 1) : 0));
|
||||
|
||||
internal ResourceTarget Resource(string id) =>
|
||||
string.Equals(id, _mainWorldResourceId, StringComparison.OrdinalIgnoreCase)
|
||||
? new ResourceTarget(World, WorldColor)
|
||||
: _resources.TryGetValue(id, out ResourceTarget? value)
|
||||
? value
|
||||
: throw new InvalidOperationException($"Resource '{id}' has no produced image.");
|
||||
|
||||
internal static TargetSet Create(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IGpuSampler sampler,
|
||||
int width,
|
||||
int height,
|
||||
int samples)
|
||||
{
|
||||
var targets = new List<IGpuRenderTarget>();
|
||||
var slots = new List<GpuTextureSlot>();
|
||||
try
|
||||
{
|
||||
bool needsDepth = descriptor.Passes.Any(pass =>
|
||||
pass.SemanticInputs.Contains(RenderSemanticInput.SceneDepth));
|
||||
IGpuRenderTarget world = device.CreateRenderTarget(new GpuRenderTargetDescription(
|
||||
$"render-pack-{descriptor.Id}-world-hdr", width, height,
|
||||
GpuTextureFormat.Rgba16FloatRenderTarget,
|
||||
GpuTextureFormat.Depth24Stencil8,
|
||||
samples,
|
||||
needsDepth));
|
||||
targets.Add(world);
|
||||
GpuTextureSlot worldColor = Register(device, world.ColorTexture, sampler, slots);
|
||||
GpuTextureSlot worldDepth = needsDepth
|
||||
? Register(device, world.DepthTexture!, sampler, slots)
|
||||
: GpuTextureSlot.Unassigned;
|
||||
var resources = new Dictionary<string, ResourceTarget>(StringComparer.OrdinalIgnoreCase);
|
||||
string? mainWorldResourceId = descriptor.Resources.SingleOrDefault(resource =>
|
||||
resource.Semantic == RenderResourceSemantic.MainWorldHdr)?.Id;
|
||||
HashSet<string> written = descriptor.Passes
|
||||
.SelectMany(pass => pass.ResourceWrites)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (RenderResourceDeclaration resource in descriptor.Resources)
|
||||
{
|
||||
if (!written.Contains(resource.Id)
|
||||
|| resource.Semantic is RenderResourceSemantic.MainWorldHdr
|
||||
or RenderResourceSemantic.DirectionalShadowDepth)
|
||||
continue;
|
||||
if (resource.Kind != RenderResourceKind.Image2D
|
||||
|| (resource.Usage & RenderResourceUsage.ColorAttachment) == 0)
|
||||
throw new NotSupportedException($"Fullscreen resource '{resource.Id}' is not a colour image.");
|
||||
(int resourceWidth, int resourceHeight) = Extent(resource, preset, width, height);
|
||||
IGpuRenderTarget target = device.CreateRenderTarget(new GpuRenderTargetDescription(
|
||||
$"render-pack-{descriptor.Id}-{resource.Id}", resourceWidth, resourceHeight,
|
||||
FormatOf(resource), null, 1));
|
||||
targets.Add(target);
|
||||
resources.Add(resource.Id, new ResourceTarget(
|
||||
target,
|
||||
Register(device, target.ColorTexture, sampler, slots)));
|
||||
}
|
||||
return new TargetSet(
|
||||
device, width, height, samples, world, worldColor, worldDepth,
|
||||
resources, [.. slots], mainWorldResourceId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
for (int i = slots.Count - 1; i >= 0; i--)
|
||||
device.ReleaseTextureSlot(slots[i]);
|
||||
for (int i = targets.Count - 1; i >= 0; i--)
|
||||
targets[i].Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
for (int i = _slots.Length - 1; i >= 0; i--)
|
||||
_device.ReleaseTextureSlot(_slots[i]);
|
||||
foreach (ResourceTarget resource in _resources.Values.Reverse())
|
||||
resource.Target.Dispose();
|
||||
World.Dispose();
|
||||
}
|
||||
|
||||
private static (int Width, int Height) Extent(
|
||||
RenderResourceDeclaration resource,
|
||||
RenderQualityPreset preset,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
RenderExtentDeclaration extent = preset.ResourceOverrides.FirstOrDefault(value =>
|
||||
string.Equals(value.ResourceId, resource.Id, StringComparison.OrdinalIgnoreCase))?.Extent
|
||||
?? resource.Extent
|
||||
?? throw new NotSupportedException($"Image resource '{resource.Id}' has no extent.");
|
||||
return extent.Mode switch
|
||||
{
|
||||
RenderExtentMode.AbsolutePixels =>
|
||||
(checked((int)extent.Width), checked((int)extent.Height)),
|
||||
RenderExtentMode.RelativeToMainWorld or RenderExtentMode.RelativeToOutput =>
|
||||
(Math.Max(1, (int)Math.Ceiling(width * extent.Width)),
|
||||
Math.Max(1, (int)Math.Ceiling(height * extent.Height))),
|
||||
_ => throw new NotSupportedException($"Resource '{resource.Id}' has unsupported extent mode."),
|
||||
};
|
||||
}
|
||||
|
||||
private static GpuTextureSlot Register(
|
||||
IGpuDevice device,
|
||||
IGpuTexture texture,
|
||||
IGpuSampler sampler,
|
||||
List<GpuTextureSlot> slots)
|
||||
{
|
||||
GpuTextureSlot slot = device.RegisterTexture(texture, sampler);
|
||||
slots.Add(slot);
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ResourceTarget(IGpuRenderTarget Target, GpuTextureSlot Slot);
|
||||
}
|
||||
|
||||
internal sealed class DeclaredDirectionalShadowRenderPackGraph :
|
||||
DeclaredFullscreenRenderPackGraph,
|
||||
IDirectionalShadowWorldGraphRuntime
|
||||
{
|
||||
internal DeclaredDirectionalShadowRenderPackGraph(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
: base(device, descriptor, assets, preset, userSettingOverrides)
|
||||
{
|
||||
}
|
||||
|
||||
internal DeclaredDirectionalShadowRenderPackGraph(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string> userSettingOverrides)
|
||||
: base(device, descriptor, assets, preset, userSettingOverrides)
|
||||
{
|
||||
}
|
||||
|
||||
public IDirectionalShadowReceiverSource DirectionalShadowReceivers =>
|
||||
DeclaredDirectionalShadowReceivers;
|
||||
|
||||
public DirectionalSunShadowDiagnostics RenderDirectionalShadows(
|
||||
IGpuFrame frame,
|
||||
in RenderFrameFoundation foundation,
|
||||
in WorldRenderFrame world,
|
||||
int activeDayGroup,
|
||||
in RenderSceneQuery scene,
|
||||
WbDrawDispatcher worldMeshes,
|
||||
TerrainModernRenderer terrain) => RenderDeclaredDirectionalShadows(
|
||||
frame,
|
||||
in foundation,
|
||||
in world,
|
||||
activeDayGroup,
|
||||
in scene,
|
||||
worldMeshes,
|
||||
terrain);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Host evaluation for the public data-only atmosphere curves. Keeping the
|
||||
/// three interpolation contracts here prevents a pack declaration from being
|
||||
/// reinterpreted differently by the declared, shadow, and volumetric graphs.
|
||||
/// </summary>
|
||||
internal static class RenderPackAtmospherePolicyEvaluation
|
||||
{
|
||||
internal static DirectionalShadowAtmospherePolicy NeutralDirectionalShadowElevation { get; } =
|
||||
DirectionalShadowAtmospherePolicy.BuiltIn with
|
||||
{
|
||||
MinimumLightElevationSin = -1.001f,
|
||||
FullStrengthLightElevationSin = -1f,
|
||||
};
|
||||
|
||||
internal static float Ray(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float elevationDegrees,
|
||||
float fallback = 1f) => Evaluate(
|
||||
points,
|
||||
elevationDegrees,
|
||||
static value => (float)value,
|
||||
static value => (float)value,
|
||||
fallback);
|
||||
|
||||
internal static float DirectionalShadow(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float elevationDegrees,
|
||||
float fallback = 0f) => DirectionalShadowFromSin(
|
||||
points,
|
||||
MathF.Sin(elevationDegrees * (MathF.PI / 180f)),
|
||||
fallback);
|
||||
|
||||
internal static float DirectionalShadowFromSin(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float lightElevationSin,
|
||||
float fallback = 0f) => Evaluate(
|
||||
points,
|
||||
Math.Clamp(lightElevationSin, -1f, 1f),
|
||||
static degrees => MathF.Sin((float)degrees * (MathF.PI / 180f)),
|
||||
static value => (float)value,
|
||||
fallback);
|
||||
|
||||
internal static float VolumetricShaft(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float elevationDegrees,
|
||||
float fallback = 0f) => Evaluate(
|
||||
points,
|
||||
elevationDegrees,
|
||||
static value => (float)value,
|
||||
static value => value * value * (3f - (2f * value)),
|
||||
fallback);
|
||||
|
||||
private static float Evaluate(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float input,
|
||||
Func<double, float> transformPoint,
|
||||
Func<float, float> transformInterpolation,
|
||||
float fallback)
|
||||
{
|
||||
if (points is null || points.Count == 0)
|
||||
return fallback;
|
||||
float first = transformPoint(points[0].ElevationDegrees);
|
||||
if (input <= first)
|
||||
return (float)points[0].Multiplier;
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
SunElevationResponsePoint upper = points[i];
|
||||
float upperInput = transformPoint(upper.ElevationDegrees);
|
||||
if (input > upperInput)
|
||||
continue;
|
||||
SunElevationResponsePoint lower = points[i - 1];
|
||||
float lowerInput = transformPoint(lower.ElevationDegrees);
|
||||
float span = upperInput - lowerInput;
|
||||
float t = span <= 0f
|
||||
? 0f
|
||||
: Math.Clamp((input - lowerInput) / span, 0f, 1f);
|
||||
t = transformInterpolation(t);
|
||||
return (float)(lower.Multiplier
|
||||
+ ((upper.Multiplier - lower.Multiplier) * t));
|
||||
}
|
||||
return (float)points[^1].Multiplier;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal static class RenderPackCapabilityResolver
|
||||
{
|
||||
internal const long AbsoluteResidentByteCeiling = 256L * 1024 * 1024;
|
||||
internal const long AbsoluteTransientByteCeiling = 512L * 1024 * 1024;
|
||||
internal const int DeviceLocalShareDenominator = 8;
|
||||
|
||||
internal static RenderPackHostCapabilities Resolve(GpuCapabilityRecord gpu)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gpu);
|
||||
var available = new HashSet<RenderCapability>
|
||||
{
|
||||
RenderCapability.FullscreenPasses,
|
||||
RenderCapability.AuthoredSunDirection,
|
||||
RenderCapability.AuthoredCelestialDirectionalLight,
|
||||
RenderCapability.AuthoredSunScreenPosition,
|
||||
RenderCapability.AuthoredWeather,
|
||||
RenderCapability.OutdoorDirectionalShadowCasterReplay,
|
||||
RenderCapability.AnimatedCasterTransforms,
|
||||
RenderCapability.AlphaCutoutShadowCasters,
|
||||
};
|
||||
|
||||
if (gpu.SupportsRgba16FloatRenderTargets)
|
||||
available.Add(RenderCapability.MainWorldColorIntermediate);
|
||||
if (gpu.SupportsSampledDepth)
|
||||
{
|
||||
available.Add(RenderCapability.SceneDepthSampling);
|
||||
available.Add(RenderCapability.DirectionalShadowMaps);
|
||||
}
|
||||
if (gpu.SupportsTimestampQueries)
|
||||
available.Add(RenderCapability.GpuTimestampQueries);
|
||||
if (gpu.SupportsMultiview)
|
||||
available.Add(RenderCapability.MultiviewDirectionalShadowCascades);
|
||||
|
||||
long residentBytes = DeviceLocalShare(
|
||||
gpu.DeviceLocalMemoryBytes,
|
||||
AbsoluteResidentByteCeiling);
|
||||
long transientBytes = DeviceLocalShare(
|
||||
gpu.DeviceLocalMemoryBytes,
|
||||
AbsoluteTransientByteCeiling);
|
||||
return new RenderPackHostCapabilities(
|
||||
available,
|
||||
MaxImageDimension2D: checked((int)Math.Min(
|
||||
gpu.MaxImageDimension2D,
|
||||
(uint)int.MaxValue)),
|
||||
MaxImageArrayLayers: checked((int)Math.Min(
|
||||
gpu.MaxImageArrayLayers,
|
||||
(uint)int.MaxValue)),
|
||||
MaxPackResidentBytes: residentBytes,
|
||||
MaxPackTransientBytes: transientBytes,
|
||||
MemoryPolicyDescription:
|
||||
$"one eighth of {gpu.DeviceLocalMemoryBytes} device-local bytes, "
|
||||
+ $"capped at {AbsoluteResidentByteCeiling} resident and "
|
||||
+ $"{AbsoluteTransientByteCeiling} transient bytes");
|
||||
}
|
||||
|
||||
private static long DeviceLocalShare(ulong deviceLocalBytes, long ceiling)
|
||||
{
|
||||
ulong share = deviceLocalBytes / DeviceLocalShareDenominator;
|
||||
return (long)Math.Min(share, checked((ulong)ceiling));
|
||||
}
|
||||
}
|
||||
37
src/AcDream.App/Rendering/Packs/RenderPackCatalogSource.cs
Normal file
37
src/AcDream.App/Rendering/Packs/RenderPackCatalogSource.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using AcDream.App.Plugins;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Production catalog authority shared by retained UI and the activation
|
||||
/// controller. It deliberately retains no catalog snapshot (and therefore no
|
||||
/// plugin asset source): withdrawal immediately releases the registry's last
|
||||
/// catalog reference, while consumers rebuild only after the revision event or
|
||||
/// an explicit UI interaction.
|
||||
/// </summary>
|
||||
internal sealed class RenderPackCatalogSource
|
||||
{
|
||||
private readonly BufferedRenderPackRegistry _registry;
|
||||
private readonly RenderPackHostCapabilities _capabilities;
|
||||
|
||||
internal RenderPackCatalogSource(
|
||||
BufferedRenderPackRegistry registry,
|
||||
RenderPackHostCapabilities capabilities)
|
||||
{
|
||||
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
_capabilities = capabilities
|
||||
?? throw new ArgumentNullException(nameof(capabilities));
|
||||
}
|
||||
|
||||
internal long Revision => _registry.Revision;
|
||||
|
||||
internal event Action<long> Changed
|
||||
{
|
||||
add => _registry.Changed += value;
|
||||
remove => _registry.Changed -= value;
|
||||
}
|
||||
|
||||
internal RenderPackCatalog Snapshot() => RenderPackCatalog.Build(
|
||||
_registry.Snapshot(),
|
||||
_capabilities);
|
||||
}
|
||||
1404
src/AcDream.App/Rendering/Packs/RenderPackController.cs
Normal file
1404
src/AcDream.App/Rendering/Packs/RenderPackController.cs
Normal file
File diff suppressed because it is too large
Load diff
284
src/AcDream.App/Rendering/Packs/RenderPackDiagnostics.cs
Normal file
284
src/AcDream.App/Rendering/Packs/RenderPackDiagnostics.cs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal readonly record struct RenderPackPassDiagnostics(
|
||||
string PassId,
|
||||
double GpuMilliseconds,
|
||||
int DrawCalls,
|
||||
int DispatchCalls);
|
||||
|
||||
/// <summary>
|
||||
/// Pack-owned facts sampled after a successful frame. Implementations expose
|
||||
/// already-resolved asynchronous timestamp results; capturing this value must
|
||||
/// never wait for the GPU.
|
||||
/// </summary>
|
||||
internal sealed record RenderPackRuntimeDiagnostics(
|
||||
string EffectiveQuality,
|
||||
long RetainedGpuBytes,
|
||||
long TransientGpuBytes,
|
||||
int ImageCount,
|
||||
int BufferCount,
|
||||
int DrawCalls,
|
||||
int DispatchCalls,
|
||||
int ShadowCasterCount,
|
||||
int CascadeDrawCount,
|
||||
int CpuClassificationCalls,
|
||||
double SunElevationDegrees,
|
||||
int ActiveDayGroup,
|
||||
string Weather,
|
||||
double WeatherIntensity,
|
||||
bool Outdoor,
|
||||
double DirectionalShadowStrength,
|
||||
IReadOnlyList<RenderPackPassDiagnostics> Passes)
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of matrices addressed through the one shared world-transform
|
||||
/// binding after the enhanced world receiver has appended its ordinary
|
||||
/// draws to the directional-shadow prefix. Zero means that no shared
|
||||
/// directional-shadow frame was active for the sampled frame.
|
||||
/// </summary>
|
||||
public uint SharedWorldTransformUsedInstances { get; init; }
|
||||
|
||||
public IReadOnlyList<RenderPackCpuStageDiagnostics> CpuStages { get; init; } = [];
|
||||
public AuthoredCelestialShadowSourceKind DirectionalShadowSourceKind
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
public int DirectionalShadowSourceObjectIndex { get; init; } = -1;
|
||||
public uint DirectionalShadowSourceGfxObjId { get; init; }
|
||||
public Vector3 DirectionalShadowSurfaceToLightDirection { get; init; }
|
||||
public float DirectionalShadowLightElevationSin { get; init; }
|
||||
public DirectionalShadowTransformChurnDiagnostics ShadowTransformChurn
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
internal static RenderPackRuntimeDiagnostics Empty(string quality) => new(
|
||||
quality,
|
||||
RetainedGpuBytes: 0,
|
||||
TransientGpuBytes: 0,
|
||||
ImageCount: 0,
|
||||
BufferCount: 0,
|
||||
DrawCalls: 0,
|
||||
DispatchCalls: 0,
|
||||
ShadowCasterCount: 0,
|
||||
CascadeDrawCount: 0,
|
||||
CpuClassificationCalls: 0,
|
||||
SunElevationDegrees: 0,
|
||||
ActiveDayGroup: -1,
|
||||
Weather: "unknown",
|
||||
WeatherIntensity: 0,
|
||||
Outdoor: false,
|
||||
DirectionalShadowStrength: 0,
|
||||
Passes: []);
|
||||
}
|
||||
|
||||
internal interface IRenderPackRuntimeDiagnosticsSource
|
||||
{
|
||||
RenderPackRuntimeDiagnostics CaptureDiagnostics();
|
||||
}
|
||||
|
||||
internal sealed record RenderPackDiagnosticsSnapshot(
|
||||
RenderPackActivationState State,
|
||||
string PackId,
|
||||
string? PackVersion,
|
||||
string PresetId,
|
||||
string EffectiveQuality,
|
||||
string? FailureReason,
|
||||
long ActivationGeneration,
|
||||
long RetainedGpuBytes,
|
||||
long TransientGpuBytes,
|
||||
int ImageCount,
|
||||
int BufferCount,
|
||||
int DrawCalls,
|
||||
int DispatchCalls,
|
||||
int ShadowCasterCount,
|
||||
int CascadeDrawCount,
|
||||
int CpuClassificationCalls,
|
||||
double SunElevationDegrees,
|
||||
int ActiveDayGroup,
|
||||
string Weather,
|
||||
double WeatherIntensity,
|
||||
bool Outdoor,
|
||||
double DirectionalShadowStrength,
|
||||
IReadOnlyList<RenderPackPassDiagnostics> Passes,
|
||||
RenderPackPerformanceSnapshot Performance = default)
|
||||
{
|
||||
public uint SharedWorldTransformUsedInstances { get; init; }
|
||||
|
||||
public IReadOnlyList<RenderPackCpuStageDiagnostics> CpuStages { get; init; } = [];
|
||||
public AuthoredCelestialShadowSourceKind DirectionalShadowSourceKind
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
public int DirectionalShadowSourceObjectIndex { get; init; } = -1;
|
||||
public uint DirectionalShadowSourceGfxObjId { get; init; }
|
||||
public Vector3 DirectionalShadowSurfaceToLightDirection { get; init; }
|
||||
public float DirectionalShadowLightElevationSin { get; init; }
|
||||
public DirectionalShadowTransformChurnDiagnostics ShadowTransformChurn
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
internal static RenderPackDiagnosticsSnapshot Retail { get; } = new(
|
||||
RenderPackActivationState.Retail,
|
||||
PackId: "retail",
|
||||
PackVersion: null,
|
||||
PresetId: "off",
|
||||
EffectiveQuality: "off",
|
||||
FailureReason: null,
|
||||
ActivationGeneration: 0,
|
||||
RetainedGpuBytes: 0,
|
||||
TransientGpuBytes: 0,
|
||||
ImageCount: 0,
|
||||
BufferCount: 0,
|
||||
DrawCalls: 0,
|
||||
DispatchCalls: 0,
|
||||
ShadowCasterCount: 0,
|
||||
CascadeDrawCount: 0,
|
||||
CpuClassificationCalls: 0,
|
||||
SunElevationDegrees: 0,
|
||||
ActiveDayGroup: -1,
|
||||
Weather: "unknown",
|
||||
WeatherIntensity: 0,
|
||||
Outdoor: false,
|
||||
DirectionalShadowStrength: 0,
|
||||
Passes: []);
|
||||
|
||||
internal bool IsRetail =>
|
||||
string.Equals(PackId, "retail", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
internal interface IRenderPackDiagnosticsSnapshotSource
|
||||
{
|
||||
RenderPackDiagnosticsSnapshot CaptureDiagnostics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construction-order bridge used by screenshot and retained-UI diagnostics.
|
||||
/// Until the render-thread controller is composed, it reports the exact
|
||||
/// resource-free retail selection.
|
||||
/// </summary>
|
||||
internal sealed class DeferredRenderPackDiagnosticsSource
|
||||
: IRenderPackDiagnosticsSnapshotSource
|
||||
{
|
||||
private IRenderPackDiagnosticsSnapshotSource? _target;
|
||||
|
||||
public RenderPackDiagnosticsSnapshot CaptureDiagnostics() =>
|
||||
_target?.CaptureDiagnostics() ?? RenderPackDiagnosticsSnapshot.Retail;
|
||||
|
||||
internal IDisposable BindOwned(IRenderPackDiagnosticsSnapshotSource target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
if (_target is not null && !ReferenceEquals(_target, target))
|
||||
throw new InvalidOperationException("Render-pack diagnostics are already bound.");
|
||||
_target = target;
|
||||
return new Binding(this, target);
|
||||
}
|
||||
|
||||
private void Unbind(IRenderPackDiagnosticsSnapshotSource target)
|
||||
{
|
||||
if (ReferenceEquals(_target, target))
|
||||
_target = null;
|
||||
}
|
||||
|
||||
private sealed class Binding(
|
||||
DeferredRenderPackDiagnosticsSource owner,
|
||||
IRenderPackDiagnosticsSnapshotSource target) : IDisposable
|
||||
{
|
||||
private DeferredRenderPackDiagnosticsSource? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Unbind(target);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RenderPackDiagnosticsFormatter
|
||||
{
|
||||
internal static string Format(RenderPackDiagnosticsSnapshot value) =>
|
||||
$"[render-pack] state={value.State} "
|
||||
+ $"pack={value.PackId}@{value.PackVersion ?? "(missing)"} "
|
||||
+ $"preset={value.PresetId} effective={value.EffectiveQuality} "
|
||||
+ $"generation={value.ActivationGeneration} "
|
||||
+ $"gpuBytes={value.RetainedGpuBytes}/{value.TransientGpuBytes} "
|
||||
+ $"resources={value.ImageCount}i/{value.BufferCount}b "
|
||||
+ $"submit={value.DrawCalls}d/{value.DispatchCalls}c "
|
||||
+ $"worldTransforms={value.SharedWorldTransformUsedInstances}used "
|
||||
+ $"shadow={value.ShadowCasterCount}casters/{value.CascadeDrawCount}cascadeDraws/"
|
||||
+ $"{value.CpuClassificationCalls}classify "
|
||||
+ $"shadowSource={value.DirectionalShadowSourceKind}/"
|
||||
+ $"obj{value.DirectionalShadowSourceObjectIndex}/"
|
||||
+ $"0x{value.DirectionalShadowSourceGfxObjId:X8}/"
|
||||
+ $"dir({Invariant(value.DirectionalShadowSurfaceToLightDirection.X, "F4")},"
|
||||
+ $"{Invariant(value.DirectionalShadowSurfaceToLightDirection.Y, "F4")},"
|
||||
+ $"{Invariant(value.DirectionalShadowSurfaceToLightDirection.Z, "F4")})/"
|
||||
+ $"elevSin={Invariant(value.DirectionalShadowLightElevationSin, "F4")} "
|
||||
+ $"atmosphere={Invariant(value.SunElevationDegrees, "F2")}deg/day{value.ActiveDayGroup}/"
|
||||
+ $"{value.Weather}:{Invariant(value.WeatherIntensity, "F3")}/outdoor={value.Outdoor}/"
|
||||
+ $"shadowStrength={Invariant(value.DirectionalShadowStrength, "F3")} "
|
||||
+ $"perf=cpu-added:{Invariant(value.Performance.IncrementalCpuMillisecondsP50, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.IncrementalCpuMillisecondsP95, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.IncrementalCpuMillisecondsP99, "F3")}ms,"
|
||||
+ $"receiver-cpu-absolute:{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP50, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP95, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP99, "F3")}ms,"
|
||||
+ $"gpu-inclusive:{Invariant(value.Performance.InclusiveGpuMillisecondsP50, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.InclusiveGpuMillisecondsP95, "F3")}/"
|
||||
+ $"{Invariant(value.Performance.InclusiveGpuMillisecondsP99, "F3")}ms "
|
||||
+ $"passes={FormatPasses(value.Passes)} "
|
||||
+ $"cpuStages={FormatCpuStages(value.CpuStages)} "
|
||||
+ $"shadowTransformChurn={FormatShadowTransformChurn(value.ShadowTransformChurn)} "
|
||||
+ $"reason={value.FailureReason ?? "none"}";
|
||||
|
||||
private static string FormatShadowTransformChurn(
|
||||
DirectionalShadowTransformChurnDiagnostics value) =>
|
||||
$"scene={value.CopiedSceneChanges}[transform={value.UpdateTransformChanges},"
|
||||
+ $"appearance={value.UpdateAppearanceChanges},sync={value.DynamicSynchronizationChanges};"
|
||||
+ $"animated={value.ActiveAnimatedStaticChanges},live={value.LiveDynamicRootChanges},"
|
||||
+ $"equipped={value.EquippedChildChanges}]/"
|
||||
+ $"casters={value.DedupedCasterSlots}/sceneFallback={value.SceneJournalFullRefresh}/"
|
||||
+ $"densityBulk={value.DensityBulkRefresh}/batchCopies={value.BatchedProjectionCopyCalls}/"
|
||||
+ $"matrices={value.ChangedMatrixSlots}/flightCurrent={value.FlightCurrentChangedMatrices}/"
|
||||
+ $"flightReplay={value.FlightPendingReplayMatrices}/uploaded={value.FlightUploadedMatrices}/"
|
||||
+ $"ranges={value.FlightUploadRanges}/bytes={value.FlightBytesWritten}/"
|
||||
+ $"flightFallback={value.FlightFullDynamicFallback}/denseDirect={value.DenseDirectUpload}/"
|
||||
+ $"denseReplay={value.DenseFlightReplay}/"
|
||||
+ $"classes=[terrain={value.CasterClasses.TerrainCommands},"
|
||||
+ $"outdoorStatic={value.CasterClasses.OutdoorStatics},"
|
||||
+ $"building={value.CasterClasses.Buildings},"
|
||||
+ $"animated={value.CasterClasses.AnimatedStatics},"
|
||||
+ $"localPlayer={value.CasterClasses.LocalPlayers},"
|
||||
+ $"remotePlayer={value.CasterClasses.RemotePlayers},"
|
||||
+ $"nonPlayerCreature={value.CasterClasses.NonPlayerCreatures},"
|
||||
+ $"otherLive={value.CasterClasses.OtherLiveDynamics},"
|
||||
+ $"equipped={value.CasterClasses.EquippedChildren}]";
|
||||
|
||||
private static string FormatPasses(IReadOnlyList<RenderPackPassDiagnostics> passes) =>
|
||||
passes.Count == 0
|
||||
? "none"
|
||||
: string.Join(
|
||||
',',
|
||||
passes.Select(static pass =>
|
||||
$"{pass.PassId}:{Invariant(pass.GpuMilliseconds, "F3")}ms/"
|
||||
+ $"{pass.DrawCalls}d/{pass.DispatchCalls}c"));
|
||||
|
||||
private static string FormatCpuStages(
|
||||
IReadOnlyList<RenderPackCpuStageDiagnostics> stages) =>
|
||||
stages.Count == 0
|
||||
? "none"
|
||||
: string.Join(
|
||||
',',
|
||||
stages.Select(static stage =>
|
||||
$"{stage.Stage}:{stage.SampleCount}n/"
|
||||
+ $"{Invariant(stage.CpuMillisecondsP50, "F3")}/"
|
||||
+ $"{Invariant(stage.CpuMillisecondsP95, "F3")}/"
|
||||
+ $"{Invariant(stage.CpuMillisecondsP99, "F3")}ms"));
|
||||
|
||||
private static string Invariant(double value, string format) =>
|
||||
value.ToString(format, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
166
src/AcDream.App/Rendering/Packs/RenderPackPerformanceWindow.cs
Normal file
166
src/AcDream.App/Rendering/Packs/RenderPackPerformanceWindow.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
using AcDream.App.Diagnostics;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal readonly record struct RenderPackPerformanceSnapshot(
|
||||
int CpuSampleCount,
|
||||
int AbsoluteReceiverCpuSampleCount,
|
||||
int GpuSampleCount,
|
||||
double IncrementalCpuMillisecondsP50,
|
||||
double IncrementalCpuMillisecondsP95,
|
||||
double IncrementalCpuMillisecondsP99,
|
||||
double AbsoluteReceiverCpuMillisecondsP50,
|
||||
double AbsoluteReceiverCpuMillisecondsP95,
|
||||
double AbsoluteReceiverCpuMillisecondsP99,
|
||||
double InclusiveGpuMillisecondsP50,
|
||||
double InclusiveGpuMillisecondsP95,
|
||||
double InclusiveGpuMillisecondsP99,
|
||||
long ResidentGpuBytes,
|
||||
long TransientGpuBytes)
|
||||
{
|
||||
internal bool HasStableAutoWindow(int minimumSamples) =>
|
||||
minimumSamples > 0
|
||||
&& CpuSampleCount >= minimumSamples
|
||||
&& GpuSampleCount >= minimumSamples;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free facts captured from the active runtime after it has
|
||||
/// submitted one complete frame. GPU time is the inclusive sum of already-
|
||||
/// resolved asynchronous pack timers, including the enhanced-world receiver
|
||||
/// pass; this contract never waits for the device.
|
||||
/// </summary>
|
||||
internal readonly record struct RenderPackRuntimePerformanceMetrics(
|
||||
long ResourceGeneration,
|
||||
bool HasResolvedGpuMeasurement,
|
||||
double InclusiveResolvedGpuMilliseconds,
|
||||
long RetainedGpuBytes,
|
||||
long TransientGpuBytes);
|
||||
|
||||
internal interface IRenderPackRuntimePerformanceSource
|
||||
{
|
||||
RenderPackRuntimePerformanceMetrics CapturePerformanceMetrics();
|
||||
}
|
||||
|
||||
internal readonly record struct RenderPackFramePerformanceObservation(
|
||||
double PackAddedCpuMilliseconds,
|
||||
bool StableFrameBoundary,
|
||||
int ViewportWidth,
|
||||
int ViewportHeight,
|
||||
int SampleCount,
|
||||
double AbsoluteEnhancedWorldReceiverCpuMilliseconds = 0d);
|
||||
|
||||
internal static class RenderPackPerformanceScopeNames
|
||||
{
|
||||
/// <summary>
|
||||
/// The enhanced main-world pass uses the pack's receiver pipelines. Its
|
||||
/// timestamp is intentionally part of the same total consumed by
|
||||
/// diagnostics and Auto; measuring only the extra shadow/post passes would
|
||||
/// hide the receiver shader's GPU cost.
|
||||
/// </summary>
|
||||
internal const string EnhancedWorldReceiver = "atmospheric-world-receiver";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free rolling evidence for one active pack runtime. Incremental
|
||||
/// CPU samples bracket only work added by the pack. The complete enhanced-world
|
||||
/// receiver recording is retained as a separate absolute diagnostic because it
|
||||
/// is not an incremental delta and must never be compared with the pack's
|
||||
/// incremental CPU budget. GPU samples are the already-resolved asynchronous
|
||||
/// total including the receiver pass for the frame that issued them. The owner
|
||||
/// resets this window on activation or quality generation changes so Auto can
|
||||
/// never compare measurements from mixed resource layouts.
|
||||
/// </summary>
|
||||
internal sealed class RenderPackPerformanceWindow
|
||||
{
|
||||
internal const int DefaultCapacity = 2048;
|
||||
|
||||
private readonly FrameStatsBuffer _cpuMicroseconds;
|
||||
private readonly FrameStatsBuffer _absoluteReceiverCpuMicroseconds;
|
||||
private readonly FrameStatsBuffer _gpuMicroseconds;
|
||||
private long _residentGpuBytes;
|
||||
private long _transientGpuBytes;
|
||||
|
||||
internal RenderPackPerformanceWindow(int capacity = DefaultCapacity)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
_cpuMicroseconds = new FrameStatsBuffer(capacity);
|
||||
_absoluteReceiverCpuMicroseconds = new FrameStatsBuffer(capacity);
|
||||
_gpuMicroseconds = new FrameStatsBuffer(capacity);
|
||||
}
|
||||
|
||||
internal void Observe(
|
||||
double incrementalCpuMilliseconds,
|
||||
double absoluteReceiverCpuMilliseconds,
|
||||
bool hasResolvedGpuMeasurement,
|
||||
double inclusiveResolvedGpuMilliseconds,
|
||||
long residentGpuBytes,
|
||||
long transientGpuBytes)
|
||||
{
|
||||
if (!double.IsFinite(incrementalCpuMilliseconds) || incrementalCpuMilliseconds < 0d)
|
||||
throw new ArgumentOutOfRangeException(nameof(incrementalCpuMilliseconds));
|
||||
if (!double.IsFinite(absoluteReceiverCpuMilliseconds)
|
||||
|| absoluteReceiverCpuMilliseconds < 0d)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(absoluteReceiverCpuMilliseconds));
|
||||
}
|
||||
if (hasResolvedGpuMeasurement
|
||||
&& (!double.IsFinite(inclusiveResolvedGpuMilliseconds)
|
||||
|| inclusiveResolvedGpuMilliseconds < 0d))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(inclusiveResolvedGpuMilliseconds));
|
||||
}
|
||||
if (residentGpuBytes < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(residentGpuBytes));
|
||||
if (transientGpuBytes < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(transientGpuBytes));
|
||||
|
||||
_cpuMicroseconds.Push(ToMicroseconds(incrementalCpuMilliseconds));
|
||||
_absoluteReceiverCpuMicroseconds.Push(
|
||||
ToMicroseconds(absoluteReceiverCpuMilliseconds));
|
||||
if (hasResolvedGpuMeasurement)
|
||||
_gpuMicroseconds.Push(ToMicroseconds(inclusiveResolvedGpuMilliseconds));
|
||||
_residentGpuBytes = residentGpuBytes;
|
||||
_transientGpuBytes = transientGpuBytes;
|
||||
}
|
||||
|
||||
internal RenderPackPerformanceSnapshot Snapshot() => new(
|
||||
_cpuMicroseconds.Count,
|
||||
_absoluteReceiverCpuMicroseconds.Count,
|
||||
_gpuMicroseconds.Count,
|
||||
ToMilliseconds(_cpuMicroseconds.Percentile(0.50)),
|
||||
ToMilliseconds(_cpuMicroseconds.Percentile(0.95)),
|
||||
ToMilliseconds(_cpuMicroseconds.Percentile(0.99)),
|
||||
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.50)),
|
||||
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.95)),
|
||||
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.99)),
|
||||
ToMilliseconds(_gpuMicroseconds.Percentile(0.50)),
|
||||
ToMilliseconds(_gpuMicroseconds.Percentile(0.95)),
|
||||
ToMilliseconds(_gpuMicroseconds.Percentile(0.99)),
|
||||
_residentGpuBytes,
|
||||
_transientGpuBytes);
|
||||
|
||||
internal int MinimumSampleCount => Math.Min(
|
||||
_cpuMicroseconds.Count,
|
||||
Math.Min(
|
||||
_absoluteReceiverCpuMicroseconds.Count,
|
||||
_gpuMicroseconds.Count));
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
_cpuMicroseconds.Reset();
|
||||
_absoluteReceiverCpuMicroseconds.Reset();
|
||||
_gpuMicroseconds.Reset();
|
||||
_residentGpuBytes = 0;
|
||||
_transientGpuBytes = 0;
|
||||
}
|
||||
|
||||
private static long ToMicroseconds(double milliseconds) =>
|
||||
checked((long)Math.Round(
|
||||
milliseconds * 1000d,
|
||||
MidpointRounding.AwayFromZero));
|
||||
|
||||
private static double ToMilliseconds(long microseconds) =>
|
||||
microseconds / 1000d;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Schedules one complete, unpublished render-pack candidate preparation.
|
||||
/// Production uses the worker scheduler so shader I/O/validation and Vulkan
|
||||
/// resource creation cannot block the render-frame boundary. Tests can inject
|
||||
/// a deterministic scheduler without adding sleeps or timing races.
|
||||
/// </summary>
|
||||
internal interface IRenderPackPreparationScheduler
|
||||
{
|
||||
Task Schedule(Action preparation);
|
||||
}
|
||||
|
||||
internal sealed class ThreadPoolRenderPackPreparationScheduler :
|
||||
IRenderPackPreparationScheduler
|
||||
{
|
||||
internal static ThreadPoolRenderPackPreparationScheduler Instance { get; } = new();
|
||||
|
||||
private ThreadPoolRenderPackPreparationScheduler()
|
||||
{
|
||||
}
|
||||
|
||||
public Task Schedule(Action preparation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(preparation);
|
||||
return Task.Run(preparation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous fixture scheduler. Production composition must use
|
||||
/// <see cref="ThreadPoolRenderPackPreparationScheduler"/>.
|
||||
/// </summary>
|
||||
internal sealed class InlineRenderPackPreparationScheduler :
|
||||
IRenderPackPreparationScheduler
|
||||
{
|
||||
internal static InlineRenderPackPreparationScheduler Instance { get; } = new();
|
||||
|
||||
private InlineRenderPackPreparationScheduler()
|
||||
{
|
||||
}
|
||||
|
||||
public Task Schedule(Action preparation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(preparation);
|
||||
preparation();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// One complete, unpublished receiver-pipeline product. Candidate resources
|
||||
/// stay owned here until the render-pack controller commits them at a stable
|
||||
/// frame boundary.
|
||||
/// </summary>
|
||||
internal interface IRenderPackReceiverPipelineCandidate : IDisposable
|
||||
{
|
||||
}
|
||||
|
||||
internal interface IRenderPackReceiverPipelineCoordinator
|
||||
{
|
||||
IRenderPackReceiverPipelineCandidate Prepare(
|
||||
IDirectionalShadowReceiverSource? source,
|
||||
int sampleCount);
|
||||
|
||||
void Publish(IRenderPackReceiverPipelineCandidate candidate);
|
||||
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Couples terrain and world-mesh receiver pipelines into the same activation
|
||||
/// transaction as their producing render-pack runtime. Preparation may compile
|
||||
/// pipelines, publication only swaps already-complete state objects, and old
|
||||
/// pipelines retire after both renderer owners point at the new generation.
|
||||
/// </summary>
|
||||
internal sealed class RenderPackReceiverPipelineCoordinator(
|
||||
TerrainModernRenderer terrain,
|
||||
WbDrawDispatcher worldMeshes) : IRenderPackReceiverPipelineCoordinator
|
||||
{
|
||||
private readonly TerrainModernRenderer _terrain = terrain
|
||||
?? throw new ArgumentNullException(nameof(terrain));
|
||||
private readonly WbDrawDispatcher _worldMeshes = worldMeshes
|
||||
?? throw new ArgumentNullException(nameof(worldMeshes));
|
||||
|
||||
public IRenderPackReceiverPipelineCandidate Prepare(
|
||||
IDirectionalShadowReceiverSource? source,
|
||||
int sampleCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sampleCount);
|
||||
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState =
|
||||
_terrain.PrepareDirectionalShadowReceiver(source, sampleCount);
|
||||
try
|
||||
{
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState =
|
||||
_worldMeshes.PrepareDirectionalShadowReceiver(source, sampleCount);
|
||||
return new Candidate(this, terrainState, worldState);
|
||||
}
|
||||
catch
|
||||
{
|
||||
terrainState?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish(IRenderPackReceiverPipelineCandidate candidate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(candidate);
|
||||
if (candidate is not Candidate prepared || !ReferenceEquals(prepared.Owner, this))
|
||||
throw new ArgumentException("Receiver candidate belongs to another coordinator.", nameof(candidate));
|
||||
|
||||
(TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState,
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState) = prepared.Take();
|
||||
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? oldTerrain =
|
||||
_terrain.SwapDirectionalShadowReceiver(terrainState);
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? oldWorld =
|
||||
_worldMeshes.SwapDirectionalShadowReceiver(worldState);
|
||||
|
||||
// Vulkan pipeline disposal is flight-fence retirement. Do this only
|
||||
// after both owners publish the complete new generation.
|
||||
oldTerrain?.Dispose();
|
||||
oldWorld?.Dispose();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? oldTerrain =
|
||||
_terrain.SwapDirectionalShadowReceiver(null);
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? oldWorld =
|
||||
_worldMeshes.SwapDirectionalShadowReceiver(null);
|
||||
oldTerrain?.Dispose();
|
||||
oldWorld?.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Candidate(
|
||||
RenderPackReceiverPipelineCoordinator owner,
|
||||
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrain,
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? world) :
|
||||
IRenderPackReceiverPipelineCandidate
|
||||
{
|
||||
private TerrainModernRenderer.DirectionalShadowReceiverPipelineState? _terrain = terrain;
|
||||
private WbDrawDispatcher.DirectionalShadowReceiverPipelineState? _world = world;
|
||||
private bool _taken;
|
||||
|
||||
internal RenderPackReceiverPipelineCoordinator Owner { get; } = owner;
|
||||
|
||||
internal (TerrainModernRenderer.DirectionalShadowReceiverPipelineState?,
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState?) Take()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_taken, this);
|
||||
_taken = true;
|
||||
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState = _terrain;
|
||||
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState = _world;
|
||||
_terrain = null;
|
||||
_world = null;
|
||||
return (terrainState, worldState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_taken)
|
||||
return;
|
||||
_taken = true;
|
||||
_terrain?.Dispose();
|
||||
_world?.Dispose();
|
||||
_terrain = null;
|
||||
_world = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using AcDream.App.Settings;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges committed Display settings to the render-thread controller. The
|
||||
/// controller performs all GPU work at the explicit frame boundary; this
|
||||
/// binding only queues stable logical selections and persists a safe retail
|
||||
/// fallback once per failed activation generation.
|
||||
/// </summary>
|
||||
internal sealed class RenderPackSelectionBinding : IDisposable
|
||||
{
|
||||
private readonly RuntimeSettingsController _settings;
|
||||
private readonly RenderPackController _controller;
|
||||
private readonly Action<string> _log;
|
||||
private long _fallbackPersistedGeneration = -1;
|
||||
private bool _suppressDisplayEdge;
|
||||
private bool _disposed;
|
||||
|
||||
internal RenderPackSelectionBinding(
|
||||
RuntimeSettingsController settings,
|
||||
RenderPackController controller,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
_log = log ?? (_ => { });
|
||||
_settings.DisplayChanged += OnDisplayChanged;
|
||||
_controller.Request(_settings.Display.RenderPack);
|
||||
}
|
||||
|
||||
internal RenderPackActivationSnapshot ApplyAtFrameBoundary(
|
||||
RenderPackActivationExtent extent)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
RenderPackActivationSnapshot snapshot = _controller.ApplyAtFrameBoundary(extent);
|
||||
if (snapshot.State != RenderPackActivationState.FailedToRetail
|
||||
|| snapshot.ActivationGeneration == _fallbackPersistedGeneration
|
||||
|| _settings.Display.RenderPack.IsRetail)
|
||||
return snapshot;
|
||||
|
||||
_fallbackPersistedGeneration = snapshot.ActivationGeneration;
|
||||
_suppressDisplayEdge = true;
|
||||
try
|
||||
{
|
||||
_settings.SaveDisplay(_settings.Display with
|
||||
{
|
||||
RenderPack = RenderPackSelectionSettings.Retail,
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressDisplayEdge = false;
|
||||
}
|
||||
|
||||
if (_settings.Display.RenderPack.IsRetail)
|
||||
{
|
||||
_log(
|
||||
$"[render-pack] selection failed; persisted acdream default (retail-faithful): "
|
||||
+ snapshot.Reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log(
|
||||
$"[render-pack] selection failed and retail fallback could not be persisted: "
|
||||
+ snapshot.Reason);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_settings.DisplayChanged -= OnDisplayChanged;
|
||||
}
|
||||
|
||||
private void OnDisplayChanged(DisplaySettings display)
|
||||
{
|
||||
if (!_disposed && !_suppressDisplayEdge)
|
||||
_controller.Request(display.RenderPack);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal static class RenderPackSettingResolution
|
||||
{
|
||||
internal static RenderPackValidationResult ValidateUserOverrides(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderPackSettingOverrides overrides)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
if (overrides is null)
|
||||
return Invalid($"Render pack '{descriptor.Id}' has a null user-setting override map.");
|
||||
|
||||
Dictionary<string, RenderSettingDeclaration> settings = descriptor.Settings
|
||||
.ToDictionary(setting => setting.Id, StringComparer.OrdinalIgnoreCase);
|
||||
foreach ((string id, string value) in overrides)
|
||||
{
|
||||
if (!settings.TryGetValue(id, out RenderSettingDeclaration? setting))
|
||||
{
|
||||
return Invalid(
|
||||
$"Render pack '{descriptor.Id}' has a user override for unknown "
|
||||
+ $"setting '{id}'.");
|
||||
}
|
||||
if (!RenderPackSettingValueCodec.TryEncode(setting, value, out _))
|
||||
{
|
||||
return Invalid(
|
||||
$"Render pack '{descriptor.Id}' user override '{id}' has invalid "
|
||||
+ $"{setting.Kind} value '{value}'.");
|
||||
}
|
||||
}
|
||||
return RenderPackValidationResult.Valid();
|
||||
}
|
||||
|
||||
internal static string Resolve(
|
||||
RenderSettingDeclaration setting,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userOverrides)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setting);
|
||||
ArgumentNullException.ThrowIfNull(preset);
|
||||
if (TryGet(userOverrides, setting.Id, out string? user))
|
||||
return user;
|
||||
RenderQualitySettingOverride? presetValue = preset.SettingOverrides
|
||||
.FirstOrDefault(value => string.Equals(
|
||||
value.SettingId,
|
||||
setting.Id,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
return presetValue?.Value ?? setting.DefaultValue;
|
||||
}
|
||||
|
||||
private static bool TryGet(
|
||||
IReadOnlyDictionary<string, string>? values,
|
||||
string id,
|
||||
out string value)
|
||||
{
|
||||
if (values is not null && values.TryGetValue(id, out value!))
|
||||
return true;
|
||||
if (values is not null)
|
||||
{
|
||||
foreach ((string key, string candidate) in values)
|
||||
{
|
||||
if (string.Equals(key, id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static RenderPackValidationResult Invalid(string reason) =>
|
||||
RenderPackValidationResult.Invalid(reason);
|
||||
}
|
||||
71
src/AcDream.App/Rendering/Packs/RenderPackShaderAssets.cs
Normal file
71
src/AcDream.App/Rendering/Packs/RenderPackShaderAssets.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal static class RenderPackShaderAssets
|
||||
{
|
||||
internal static ValidatedRenderPackShaderAssets Validate(
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets)
|
||||
{
|
||||
RenderPackValidationResult result = RenderPackValidator.ValidateSelectedAssets(
|
||||
descriptor,
|
||||
assets,
|
||||
out ValidatedRenderPackShaderAssets? validated);
|
||||
if (!result.Success)
|
||||
throw new InvalidDataException(result.Reason);
|
||||
return validated!;
|
||||
}
|
||||
|
||||
internal static GpuShaderSet LoadPass(
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets,
|
||||
RenderPassDeclaration pass) => new(
|
||||
$"{descriptor.Id}:{pass.Id}",
|
||||
assets.Copy(pass.VertexShaderAsset),
|
||||
assets.Copy(pass.FragmentShaderAsset));
|
||||
|
||||
internal static GpuShaderSet LoadVariant(
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets,
|
||||
PipelineVariantDeclaration variant) => new(
|
||||
$"{descriptor.Id}:{variant.Id}",
|
||||
assets.Copy(variant.VertexShaderAsset),
|
||||
assets.Copy(variant.FragmentShaderAsset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Candidate-owned immutable shader snapshot. The plugin asset provider is
|
||||
/// read exactly once during selected-candidate validation; pipeline creation
|
||||
/// only copies bytes from this snapshot and cannot reopen a mutable plugin
|
||||
/// stream or resolve a second path.
|
||||
/// </summary>
|
||||
internal sealed class ValidatedRenderPackShaderAssets
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, ImmutableArray<byte>> _assets;
|
||||
|
||||
internal ValidatedRenderPackShaderAssets(
|
||||
IReadOnlyDictionary<string, byte[]> assets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
var owned = new Dictionary<string, ImmutableArray<byte>>(
|
||||
assets.Count,
|
||||
StringComparer.Ordinal);
|
||||
foreach ((string key, byte[] bytes) in assets)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
ArgumentNullException.ThrowIfNull(bytes);
|
||||
owned.Add(key, [.. bytes]);
|
||||
}
|
||||
_assets = owned;
|
||||
}
|
||||
|
||||
internal byte[] Copy(string key)
|
||||
{
|
||||
if (!_assets.TryGetValue(key, out ImmutableArray<byte> bytes))
|
||||
throw new InvalidDataException($"Validated render-pack shader '{key}' is missing.");
|
||||
return [.. bytes];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal readonly record struct RenderPackTextureInput(
|
||||
RenderSemanticInput? Semantic,
|
||||
string? ResourceId)
|
||||
{
|
||||
internal static RenderPackTextureInput FromSemantic(RenderSemanticInput value) =>
|
||||
new(value, null);
|
||||
|
||||
internal static RenderPackTextureInput FromResource(string value) =>
|
||||
new(null, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary API-v1 texture-slot rule. Ordinary sampled inputs occupy push
|
||||
/// TextureIndexA..D in declaration order: sampled semantic inputs first, then
|
||||
/// declared resource reads. Directional depth uses its dedicated binding-6
|
||||
/// texture slot and therefore does not consume A..D.
|
||||
/// </summary>
|
||||
internal static class RenderPackTextureBindingResolver
|
||||
{
|
||||
internal static IReadOnlyList<RenderPackTextureInput> Resolve(
|
||||
RenderPassDeclaration pass,
|
||||
IReadOnlyDictionary<string, RenderResourceDeclaration> resources)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pass);
|
||||
ArgumentNullException.ThrowIfNull(resources);
|
||||
var result = new List<RenderPackTextureInput>(4);
|
||||
foreach (RenderSemanticInput semantic in pass.SemanticInputs)
|
||||
{
|
||||
if (semantic is RenderSemanticInput.WorldColor
|
||||
or RenderSemanticInput.SceneDepth
|
||||
or RenderSemanticInput.SceneNormals)
|
||||
result.Add(RenderPackTextureInput.FromSemantic(semantic));
|
||||
}
|
||||
foreach (string resourceId in pass.ResourceReads)
|
||||
{
|
||||
if (!resources.TryGetValue(resourceId, out RenderResourceDeclaration? resource))
|
||||
throw new InvalidOperationException($"Unknown render-pack resource '{resourceId}'.");
|
||||
if (resource.Format == RenderFormatClass.DirectionalDepth
|
||||
&& pass.SemanticInputs.Contains(RenderSemanticInput.DirectionalShadowMaps))
|
||||
continue;
|
||||
result.Add(RenderPackTextureInput.FromResource(resourceId));
|
||||
}
|
||||
if (result.Count > 4)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Render-pack pass '{pass.Id}' exceeds the four API-v1 texture slots.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
1604
src/AcDream.App/Rendering/Packs/RenderPackValidation.cs
Normal file
1604
src/AcDream.App/Rendering/Packs/RenderPackValidation.cs
Normal file
File diff suppressed because it is too large
Load diff
471
src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs
Normal file
471
src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
internal enum VolumetricShaftGateReason : byte
|
||||
{
|
||||
Rendered,
|
||||
DisabledByPreset,
|
||||
NoCurrentDirectionalShadow,
|
||||
NoSceneDepth,
|
||||
Indoor,
|
||||
SunOffScreen,
|
||||
SunBelowHorizon,
|
||||
AtmosphereSuppressed,
|
||||
}
|
||||
|
||||
internal readonly record struct VolumetricShaftDiagnostics(
|
||||
VolumetricShaftGateReason GateReason,
|
||||
int Width,
|
||||
int Height,
|
||||
int RayMarchSteps,
|
||||
float Density,
|
||||
float Strength,
|
||||
long RetainedGpuBytes,
|
||||
double LastResolvedGpuMilliseconds,
|
||||
bool HasResolvedGpuMeasurement,
|
||||
int DrawCalls);
|
||||
|
||||
internal readonly record struct VolumetricShaftOutput(
|
||||
GpuTextureSlot TextureSlot,
|
||||
VolumetricShaftDiagnostics Diagnostics)
|
||||
{
|
||||
internal bool HasTexture => TextureSlot.IsAssigned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tier-2+ shadow-map volumetric producer. It consumes only the current frame's
|
||||
/// b5/b6/b8 facts and scene depth, and owns one preset-scaled HDR result. It has
|
||||
/// no clock, weather state, caster traversal, or independent sun policy.
|
||||
/// </summary>
|
||||
internal sealed class VolumetricShaftRenderer : IDisposable
|
||||
{
|
||||
internal const string TimerName = "atmospheric-volumetric-shafts";
|
||||
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly VolumetricShaftQuality _quality;
|
||||
private readonly float _declaredStrength;
|
||||
private readonly AtmospherePolicyDeclaration _atmospherePolicy;
|
||||
private readonly IReadOnlyDictionary<int, float> _dayGroupMultipliers;
|
||||
private readonly IGpuSampler _sampler;
|
||||
private readonly IGpuPipeline _pipeline;
|
||||
private readonly PackSettingsUniforms _settings;
|
||||
private readonly RenderPackPerformanceWindow _performance = new();
|
||||
private Target? _target;
|
||||
private bool _disposed;
|
||||
|
||||
internal VolumetricShaftRenderer(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
: this(
|
||||
device,
|
||||
descriptor,
|
||||
RenderPackShaderAssets.Validate(descriptor, assets),
|
||||
preset,
|
||||
userSettingOverrides)
|
||||
{
|
||||
}
|
||||
|
||||
internal VolumetricShaftRenderer(
|
||||
IGpuDevice device,
|
||||
RenderPackDescriptor descriptor,
|
||||
ValidatedRenderPackShaderAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
ArgumentNullException.ThrowIfNull(preset);
|
||||
_quality = ResolveQuality(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
_declaredStrength = ReadSetting(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides,
|
||||
RenderSettingSemantic.VolumetricStrength,
|
||||
0.35f);
|
||||
_atmospherePolicy = descriptor.AtmospherePolicy
|
||||
?? throw new NotSupportedException(
|
||||
$"Pack '{descriptor.Id}' declares no atmosphere policy.");
|
||||
if (_atmospherePolicy.VolumetricShaftSunElevationResponse.Count < 2)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Pack '{descriptor.Id}' declares no volumetric-shaft elevation curve.");
|
||||
}
|
||||
_dayGroupMultipliers = _atmospherePolicy.ActiveDayGroupMultipliers
|
||||
.ToDictionary(value => value.ActiveDayGroup, value => (float)value.Multiplier);
|
||||
_settings = PackSettingsUniforms.Create(descriptor, preset, userSettingOverrides);
|
||||
RenderPassDeclaration pass = descriptor.Passes.FirstOrDefault(value =>
|
||||
value.Semantic == RenderPassSemantic.VolumetricShafts)
|
||||
?? throw new NotSupportedException(
|
||||
$"Pack '{descriptor.Id}' declares no VolumetricShafts pass semantic.");
|
||||
|
||||
_sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
||||
_pipeline = device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = $"render-pack-{descriptor.Id}-volumetric-shafts",
|
||||
Shaders = RenderPackShaderAssets.LoadPass(descriptor, assets, pass),
|
||||
VertexLayout = GpuVertexLayout.None,
|
||||
Blend = GpuBlendMode.None,
|
||||
Depth = GpuDepthState.Disabled,
|
||||
Cull = GpuCullMode.None,
|
||||
ColorFormat = GpuTextureFormat.Rgba16FloatRenderTarget,
|
||||
AllowColorFormatVariants = false,
|
||||
SampleCount = 1,
|
||||
UsesRenderPackShaderAbi = true,
|
||||
});
|
||||
LastDiagnostics = Disabled(VolumetricShaftGateReason.DisabledByPreset);
|
||||
}
|
||||
|
||||
internal VolumetricShaftDiagnostics LastDiagnostics { get; private set; }
|
||||
|
||||
internal VolumetricShaftQuality Quality => _quality;
|
||||
|
||||
internal RenderPackPerformanceSnapshot Performance => _performance.Snapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Builds the selected preset's optional shaft target during off-side pack
|
||||
/// activation/resize. A disabled preset owns no target; enabling it later
|
||||
/// through a user override is reflected in <see cref="_declaredStrength"/>.
|
||||
/// </summary>
|
||||
internal void PrepareTarget(int outputWidth, int outputHeight)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputWidth);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputHeight);
|
||||
if (_declaredStrength > 0f)
|
||||
_ = Prepare(outputWidth, outputHeight);
|
||||
}
|
||||
|
||||
internal VolumetricShaftOutput Render(
|
||||
IGpuFrame frame,
|
||||
in AtmosphericFrameInputs inputs,
|
||||
in DirectionalShadowFrameBinding shadow,
|
||||
GpuTextureSlot sceneDepth)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
VolumetricShaftGateReason reason = Gate(frame, inputs, shadow, sceneDepth);
|
||||
if (reason != VolumetricShaftGateReason.Rendered)
|
||||
{
|
||||
LastDiagnostics = Disabled(reason);
|
||||
return new VolumetricShaftOutput(GpuTextureSlot.Unassigned, LastDiagnostics);
|
||||
}
|
||||
|
||||
(float density, float strength) = Parameters(inputs);
|
||||
if (strength <= 1e-4f)
|
||||
{
|
||||
LastDiagnostics = Disabled(VolumetricShaftGateReason.AtmosphereSuppressed);
|
||||
return new VolumetricShaftOutput(GpuTextureSlot.Unassigned, LastDiagnostics);
|
||||
}
|
||||
|
||||
Target target = Prepare(inputs.ViewportWidth, inputs.ViewportHeight);
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
AtmosphericFrameUniforms atmospheric = FrameUniforms(inputs, strength);
|
||||
GpuRingAllocation frameBlock = frame.AllocateRing(
|
||||
AtmosphericFrameUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
MemoryMarshal.Write(frameBlock.Data, in atmospheric);
|
||||
GpuRingAllocation passBlock = frame.AllocateRing(
|
||||
AtmosphericPackPassUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
var passValues = new AtmosphericPackPassUniforms(
|
||||
new Vector4(density, strength, _quality.RayMarchSteps, 1f),
|
||||
Vector4.Zero,
|
||||
Vector4.Zero,
|
||||
Vector4.Zero);
|
||||
MemoryMarshal.Write(passBlock.Data, in passValues);
|
||||
GpuRingAllocation settingsBlock = frame.AllocateRing(
|
||||
PackSettingsUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
PackSettingsUniforms settings = _settings;
|
||||
MemoryMarshal.Write(settingsBlock.Data, in settings);
|
||||
|
||||
using (IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
Name = TimerName,
|
||||
Color = new GpuColorAttachment(
|
||||
target.RenderTarget,
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
Vector4.Zero),
|
||||
Depth = null,
|
||||
SampleCount = 1,
|
||||
}))
|
||||
using (encoder.BeginTimerScope(TimerName))
|
||||
{
|
||||
encoder.BindPipeline(_pipeline);
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformAtmosphericFrame,
|
||||
frameBlock.Buffer,
|
||||
frameBlock.OffsetBytes,
|
||||
AtmosphericFrameUniforms.SizeInBytes);
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
shadow.Buffer!,
|
||||
shadow.OffsetBytes,
|
||||
shadow.SizeBytes);
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformPackPass,
|
||||
passBlock.Buffer,
|
||||
passBlock.OffsetBytes,
|
||||
AtmosphericPackPassUniforms.SizeInBytes);
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformPackSettings,
|
||||
settingsBlock.Buffer,
|
||||
settingsBlock.OffsetBytes,
|
||||
PackSettingsUniforms.SizeInBytes);
|
||||
GpuPushConstants push = GpuPushConstants.Default;
|
||||
push.TextureIndexA = sceneDepth.Index;
|
||||
push.TextureIndexB = GpuTextureSlot.Unassigned.Index;
|
||||
push.ParamA = BitConverter.UInt32BitsToSingle(GpuTextureSlot.Unassigned.Index);
|
||||
push.ParamB = BitConverter.UInt32BitsToSingle(GpuTextureSlot.Unassigned.Index);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.Draw(3, 1, 0, 0);
|
||||
}
|
||||
|
||||
bool hasGpu = _device.Timers.TryResolve(TimerName, out double milliseconds);
|
||||
LastDiagnostics = new VolumetricShaftDiagnostics(
|
||||
VolumetricShaftGateReason.Rendered,
|
||||
target.RenderTarget.Description.Width,
|
||||
target.RenderTarget.Description.Height,
|
||||
_quality.RayMarchSteps,
|
||||
density,
|
||||
strength,
|
||||
target.RetainedBytes,
|
||||
milliseconds,
|
||||
hasGpu,
|
||||
DrawCalls: 1);
|
||||
_performance.Observe(
|
||||
Stopwatch.GetElapsedTime(started).TotalMilliseconds,
|
||||
absoluteReceiverCpuMilliseconds: 0d,
|
||||
hasGpu,
|
||||
milliseconds,
|
||||
target.RetainedBytes,
|
||||
transientGpuBytes: 0);
|
||||
return new VolumetricShaftOutput(target.TextureSlot, LastDiagnostics);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_target?.Dispose();
|
||||
_target = null;
|
||||
_pipeline.Dispose();
|
||||
}
|
||||
|
||||
private Target Prepare(int outputWidth, int outputHeight)
|
||||
{
|
||||
int width = Math.Max(1, (int)MathF.Ceiling(outputWidth * _quality.ResolutionScale));
|
||||
int height = Math.Max(1, (int)MathF.Ceiling(outputHeight * _quality.ResolutionScale));
|
||||
if (_target is { } current
|
||||
&& current.RenderTarget.Description.Width == width
|
||||
&& current.RenderTarget.Description.Height == height)
|
||||
return current;
|
||||
|
||||
IGpuRenderTarget? renderTarget = null;
|
||||
GpuTextureSlot slot = GpuTextureSlot.Unassigned;
|
||||
try
|
||||
{
|
||||
renderTarget = _device.CreateRenderTarget(new GpuRenderTargetDescription(
|
||||
"atmospheric-volumetric",
|
||||
width,
|
||||
height,
|
||||
GpuTextureFormat.Rgba16FloatRenderTarget,
|
||||
DepthFormat: null,
|
||||
SampleCount: 1));
|
||||
slot = _device.RegisterTexture(renderTarget.ColorTexture, _sampler);
|
||||
var candidate = new Target(_device, renderTarget, slot);
|
||||
renderTarget = null;
|
||||
slot = GpuTextureSlot.Unassigned;
|
||||
Target? prior = _target;
|
||||
_target = candidate;
|
||||
prior?.Dispose();
|
||||
_performance.Reset();
|
||||
return candidate;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (slot.IsAssigned)
|
||||
_device.ReleaseTextureSlot(slot);
|
||||
renderTarget?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private VolumetricShaftGateReason Gate(
|
||||
IGpuFrame frame,
|
||||
in AtmosphericFrameInputs inputs,
|
||||
in DirectionalShadowFrameBinding shadow,
|
||||
GpuTextureSlot sceneDepth)
|
||||
{
|
||||
if (_declaredStrength <= 0f)
|
||||
return VolumetricShaftGateReason.DisabledByPreset;
|
||||
if (!shadow.IsValidFor(frame))
|
||||
return VolumetricShaftGateReason.NoCurrentDirectionalShadow;
|
||||
if (!sceneDepth.IsAssigned)
|
||||
return VolumetricShaftGateReason.NoSceneDepth;
|
||||
if (!inputs.IsOutdoor)
|
||||
return VolumetricShaftGateReason.Indoor;
|
||||
if (!inputs.SunIsOnScreen)
|
||||
return VolumetricShaftGateReason.SunOffScreen;
|
||||
return VolumetricShaftGateReason.Rendered;
|
||||
}
|
||||
|
||||
private (float Density, float Strength) Parameters(in AtmosphericFrameInputs inputs)
|
||||
{
|
||||
float weatherTarget = inputs.Weather switch
|
||||
{
|
||||
AcDream.Core.World.WeatherKind.Clear => 1f,
|
||||
AcDream.Core.World.WeatherKind.Overcast => 0.18f,
|
||||
AcDream.Core.World.WeatherKind.Rain => 0.10f,
|
||||
AcDream.Core.World.WeatherKind.Snow => 0.16f,
|
||||
AcDream.Core.World.WeatherKind.Storm => 0.06f,
|
||||
_ => 0f,
|
||||
};
|
||||
float weatherBlend = Math.Clamp(inputs.WeatherIntensity, 0f, 1f);
|
||||
float weather = 1f + ((weatherTarget - 1f) * weatherBlend);
|
||||
float elevation = RenderPackAtmospherePolicyEvaluation.VolumetricShaft(
|
||||
_atmospherePolicy.VolumetricShaftSunElevationResponse,
|
||||
inputs.SunElevationDegrees);
|
||||
float authoredEnergy = Math.Clamp(inputs.SunDirectionalBrightness, 0f, 4f);
|
||||
float dayGroup = _dayGroupMultipliers.TryGetValue(
|
||||
inputs.ActiveDayGroup,
|
||||
out float declaredDayGroup)
|
||||
? Math.Clamp(declaredDayGroup, 0f, 4f)
|
||||
: 1f;
|
||||
float strength = Math.Clamp(
|
||||
_declaredStrength * weather * elevation * authoredEnergy * dayGroup,
|
||||
0f,
|
||||
1f);
|
||||
return (0.035f * strength, strength);
|
||||
}
|
||||
|
||||
private AtmosphericFrameUniforms FrameUniforms(
|
||||
in AtmosphericFrameInputs inputs,
|
||||
float strength) => new(
|
||||
new Vector4(inputs.SunScreenUv, strength, inputs.SunElevationDegrees),
|
||||
new Vector4(inputs.SunColor, strength),
|
||||
new Vector4(inputs.ViewportWidth, inputs.ViewportHeight,
|
||||
1f / inputs.ViewportWidth, 1f / inputs.ViewportHeight),
|
||||
new Vector4((float)inputs.Weather, inputs.WeatherIntensity,
|
||||
(float)Math.Clamp(inputs.DeltaSeconds, 0d, 1d), inputs.IsOutdoor ? 1f : 0f),
|
||||
new Vector4(inputs.SunDirection, inputs.SunDirectionalBrightness),
|
||||
new Vector4(
|
||||
inputs.ActiveDayGroup,
|
||||
_dayGroupMultipliers.TryGetValue(inputs.ActiveDayGroup, out float dayGroup)
|
||||
? dayGroup
|
||||
: 1f,
|
||||
RenderPackAtmospherePolicyEvaluation.DirectionalShadow(
|
||||
_atmospherePolicy.DirectionalShadowLightElevationResponse,
|
||||
inputs.SunElevationDegrees),
|
||||
RenderPackAtmospherePolicyEvaluation.VolumetricShaft(
|
||||
_atmospherePolicy.VolumetricShaftSunElevationResponse,
|
||||
inputs.SunElevationDegrees)),
|
||||
inputs.InverseViewProjection);
|
||||
|
||||
private VolumetricShaftDiagnostics Disabled(VolumetricShaftGateReason reason) => new(
|
||||
reason,
|
||||
0,
|
||||
0,
|
||||
_quality.RayMarchSteps,
|
||||
0f,
|
||||
0f,
|
||||
_target?.RetainedBytes ?? 0L,
|
||||
0d,
|
||||
false,
|
||||
0);
|
||||
|
||||
private static DirectionalShadowPreset PresetOf(RenderQualityPreset preset) =>
|
||||
preset.Semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => DirectionalShadowPreset.Low,
|
||||
RenderQualitySemantic.High => DirectionalShadowPreset.High,
|
||||
_ => DirectionalShadowPreset.Medium,
|
||||
};
|
||||
|
||||
private static VolumetricShaftQuality ResolveQuality(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides)
|
||||
{
|
||||
VolumetricShaftQuality quality = VolumetricShaftQuality.For(PresetOf(preset));
|
||||
RenderResourceDeclaration resource = descriptor.Resources.Single(value =>
|
||||
value.Semantic == RenderResourceSemantic.VolumetricShafts);
|
||||
RenderQualityResourceOverride? resourceOverride = preset.ResourceOverrides
|
||||
.FirstOrDefault(value => string.Equals(
|
||||
value.ResourceId,
|
||||
resource.Id,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
RenderExtentDeclaration extent = resourceOverride?.Extent
|
||||
?? resource.Extent
|
||||
?? throw new NotSupportedException(
|
||||
"The VolumetricShafts semantic resource has no image extent.");
|
||||
if (extent.Mode is not RenderExtentMode.RelativeToMainWorld
|
||||
and not RenderExtentMode.RelativeToOutput)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The VolumetricShafts semantic resource must use a relative extent.");
|
||||
}
|
||||
int steps = checked((int)MathF.Round(ReadSetting(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides,
|
||||
RenderSettingSemantic.VolumetricRayMarchSteps,
|
||||
quality.RayMarchSteps)));
|
||||
return quality with
|
||||
{
|
||||
ResolutionScale = (float)Math.Clamp(extent.Width, 0.0625, 1.0),
|
||||
RayMarchSteps = Math.Clamp(steps, 8, 64),
|
||||
};
|
||||
}
|
||||
|
||||
private static float ReadSetting(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides,
|
||||
RenderSettingSemantic semantic,
|
||||
float fallback)
|
||||
{
|
||||
RenderSettingDeclaration? setting = descriptor.Settings.FirstOrDefault(candidate =>
|
||||
candidate.Semantic == semantic);
|
||||
if (setting is null)
|
||||
return fallback;
|
||||
string value = RenderPackSettingResolution.Resolve(
|
||||
setting,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
return RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
|
||||
? Math.Max(0f, encoded)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private sealed class Target(
|
||||
IGpuDevice device,
|
||||
IGpuRenderTarget renderTarget,
|
||||
GpuTextureSlot textureSlot) : IDisposable
|
||||
{
|
||||
internal IGpuRenderTarget RenderTarget { get; } = renderTarget;
|
||||
internal GpuTextureSlot TextureSlot { get; } = textureSlot;
|
||||
internal long RetainedBytes => checked(
|
||||
(long)RenderTarget.Description.Width * RenderTarget.Description.Height * 8L);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
device.ReleaseTextureSlot(TextureSlot);
|
||||
RenderTarget.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -153,6 +154,7 @@ internal sealed class RenderFrameDiagnosticsController :
|
|||
private readonly IRenderFrameResourceDiagnosticsSource? _resources;
|
||||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private readonly bool _publishResourceDiagnostics;
|
||||
private readonly IRenderPackDiagnosticsSnapshotSource? _renderPack;
|
||||
|
||||
private double _elapsedSeconds;
|
||||
private int _frameCount;
|
||||
|
|
@ -165,7 +167,8 @@ internal sealed class RenderFrameDiagnosticsController :
|
|||
IRenderFrameTitleSink titleSink,
|
||||
IRenderFrameDiagnosticLog log,
|
||||
bool publishResourceDiagnostics,
|
||||
IRenderFrameResourceDiagnosticsSource? resources = null)
|
||||
IRenderFrameResourceDiagnosticsSource? resources = null,
|
||||
IRenderPackDiagnosticsSnapshotSource? renderPack = null)
|
||||
{
|
||||
_titleFacts = titleFacts ?? throw new ArgumentNullException(nameof(titleFacts));
|
||||
_titleSink = titleSink ?? throw new ArgumentNullException(nameof(titleSink));
|
||||
|
|
@ -174,6 +177,7 @@ internal sealed class RenderFrameDiagnosticsController :
|
|||
_resources = publishResourceDiagnostics
|
||||
? resources ?? throw new ArgumentNullException(nameof(resources))
|
||||
: resources;
|
||||
_renderPack = renderPack;
|
||||
}
|
||||
|
||||
public void Publish(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
|
|
@ -201,6 +205,11 @@ internal sealed class RenderFrameDiagnosticsController :
|
|||
{
|
||||
RenderFrameResourceDiagnosticsSnapshot resources = _resources!.Capture();
|
||||
_log.WriteLine(FormatGpuStream(resources));
|
||||
if (_renderPack is not null)
|
||||
{
|
||||
_log.WriteLine(RenderPackDiagnosticsFormatter.Format(
|
||||
_renderPack.CaptureDiagnostics()));
|
||||
}
|
||||
}
|
||||
|
||||
Snapshot = new RenderFrameDiagnosticsSnapshot(
|
||||
|
|
|
|||
47
src/AcDream.App/Rendering/RetailDetailTextureContract.cs
Normal file
47
src/AcDream.App/Rendering/RetailDetailTextureContract.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Testable CPU statement of retail's detail-pass gate and pixel math. The
|
||||
/// production pixels are produced by <c>mesh_detail</c>; keeping these facts in
|
||||
/// one small contract makes the setting, distance units, neutral point, and
|
||||
/// intentional brightening independently assertable without a GPU.
|
||||
/// </summary>
|
||||
internal static class RetailDetailTextureContract
|
||||
{
|
||||
internal const float FullDetailDistanceMetres = 10f;
|
||||
internal const float ZeroDetailDistanceMetres = 50f;
|
||||
|
||||
internal static bool ShouldRender(
|
||||
bool settingEnabled,
|
||||
TerrainAtlas.RetailDetailTextureBinding binding) =>
|
||||
settingEnabled && binding.IsAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Opaque detail must compare equal against the depth written by its exact
|
||||
/// base geometry. On an MSAA target that inherits the base pass's per-sample
|
||||
/// alpha-to-coverage mask without applying A2C to the detail alpha itself.
|
||||
/// Transparent bases do not write depth, so their adjacent detail uses the
|
||||
/// accepted less-or-equal comparison instead.
|
||||
/// </summary>
|
||||
internal static GpuCompareOp DetailDepthCompare(bool transparent) =>
|
||||
transparent ? GpuCompareOp.LessOrEqual : GpuCompareOp.Equal;
|
||||
|
||||
internal static float FadeForPositiveViewDepthMetres(float depthMetres) =>
|
||||
Math.Clamp(
|
||||
(ZeroDetailDistanceMetres - depthMetres)
|
||||
/ (ZeroDetailDistanceMetres - FullDetailDistanceMetres),
|
||||
0f,
|
||||
1f);
|
||||
|
||||
/// <summary>
|
||||
/// Effective multiplier on the existing framebuffer after the shader
|
||||
/// scales both detail RGB and alpha by fade and the pipeline applies
|
||||
/// <c>DstColor + OneMinusSrcAlpha</c>.
|
||||
/// </summary>
|
||||
internal static Vector3 FramebufferFactor(Vector4 detail, float fade) =>
|
||||
Vector3.One + fade * (new Vector3(detail.X, detail.Y, detail.Z)
|
||||
- new Vector3(detail.W));
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
using Arch.Core;
|
||||
using ArchWorld = Arch.Core.World;
|
||||
|
|
@ -37,6 +39,12 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
private RenderProjectionCounts _counts;
|
||||
private ulong _lastAppliedJournalSequence;
|
||||
private ulong _indexRevision = 1;
|
||||
private ulong _directionalShadowTopologyRevision = 1;
|
||||
private DirectionalShadowTransformChange[]? _directionalShadowTransformChanges;
|
||||
private Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
|
||||
_directionalShadowPartPoses;
|
||||
private ulong _directionalShadowTransformRevision;
|
||||
private int _directionalShadowTransformChangeCount;
|
||||
private bool _disposed;
|
||||
|
||||
public ArchRenderScene(RenderSceneGeneration initialGeneration)
|
||||
|
|
@ -76,6 +84,23 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
long lookupBytes =
|
||||
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
|
||||
long indexBytes = EstimateIndexBytes();
|
||||
long directionalShadowJournalBytes =
|
||||
_directionalShadowTransformChanges is null
|
||||
? 0
|
||||
: checked((long)_directionalShadowTransformChanges.Length
|
||||
* Unsafe.SizeOf<DirectionalShadowTransformChange>());
|
||||
if (_directionalShadowPartPoses is not null)
|
||||
{
|
||||
directionalShadowJournalBytes = checked(
|
||||
directionalShadowJournalBytes
|
||||
+ (long)_directionalShadowPartPoses.EnsureCapacity(0)
|
||||
* (sizeof(int)
|
||||
+ Unsafe.SizeOf<KeyValuePair<
|
||||
RenderProjectionId,
|
||||
DirectionalShadowPartPoseSnapshot>>())
|
||||
+ _directionalShadowPartPoses.Values.Sum(static pose =>
|
||||
(long)pose.Count * Unsafe.SizeOf<Matrix4x4>()));
|
||||
}
|
||||
|
||||
return new RenderSceneMemoryAccounting(
|
||||
EntityCount: _world.Size,
|
||||
|
|
@ -86,7 +111,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
ProjectionLookupCapacity: lookupCapacity,
|
||||
EstimatedProjectionLookupBytes: lookupBytes,
|
||||
EstimatedIndexBytes: indexBytes,
|
||||
EstimatedJournalBufferBytes: 0,
|
||||
EstimatedJournalBufferBytes: directionalShadowJournalBytes,
|
||||
EstimatedSynchronizationSourceBytes: 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +188,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
ref _world.Get<RenderTransform>(entry.Entity);
|
||||
ref RenderWorldBounds bounds =
|
||||
ref _world.Get<RenderWorldBounds>(entry.Entity);
|
||||
if (current == update.Transform && bounds == update.Bounds)
|
||||
bool transformChanged = !TransformBitsEqual(current, update.Transform);
|
||||
if (!transformChanged && bounds == update.Bounds)
|
||||
continue;
|
||||
|
||||
_world.Set(
|
||||
|
|
@ -171,6 +197,15 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
new PreviousRenderTransform(current.LocalToWorld));
|
||||
_world.Set(entry.Entity, update.Transform);
|
||||
_world.Set(entry.Entity, update.Bounds);
|
||||
if (transformChanged
|
||||
&& HasRefreshableDirectionalShadowTransforms(entry.ProjectionClass)
|
||||
&& _directionalShadowTransformChanges is not null)
|
||||
{
|
||||
RenderProjectionRecord currentRecord = ReadRecord(in entry);
|
||||
PublishDirectionalShadowTransformChange(
|
||||
in currentRecord,
|
||||
DirectionalShadowTransformChangeKind.DynamicSynchronization);
|
||||
}
|
||||
|
||||
ref RenderDirtyMask dirty =
|
||||
ref _world.Get<RenderDirtyMask>(entry.Entity);
|
||||
|
|
@ -226,8 +261,10 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
ClearIndices();
|
||||
_counts = default;
|
||||
_lastAppliedJournalSequence = 0;
|
||||
ResetDirectionalShadowTransformChanges();
|
||||
Generation = replacementGeneration;
|
||||
AdvanceIndexRevision();
|
||||
AdvanceDirectionalShadowTopologyRevision();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -239,6 +276,10 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
ArchWorld.Destroy(_world);
|
||||
_entries.Clear();
|
||||
ClearIndices();
|
||||
_directionalShadowTransformChanges = null;
|
||||
_directionalShadowPartPoses = null;
|
||||
_directionalShadowTransformRevision = 0;
|
||||
_directionalShadowTransformChangeCount = 0;
|
||||
_counts = default;
|
||||
_disposed = true;
|
||||
}
|
||||
|
|
@ -284,6 +325,99 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
return _indexRevision;
|
||||
}
|
||||
|
||||
ulong IRenderSceneQuerySource.GetDirectionalShadowTopologyRevision(
|
||||
RenderSceneGeneration generation)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
return _directionalShadowTopologyRevision;
|
||||
}
|
||||
|
||||
ulong IRenderSceneQuerySource.GetDirectionalShadowTransformRevision(
|
||||
RenderSceneGeneration generation)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
EnsureDirectionalShadowTransformJournal();
|
||||
return _directionalShadowTransformRevision;
|
||||
}
|
||||
|
||||
DirectionalShadowTransformChanges
|
||||
IRenderSceneQuerySource.CopyDirectionalShadowTransformChanges(
|
||||
RenderSceneGeneration generation,
|
||||
ulong afterRevision,
|
||||
Span<DirectionalShadowTransformSnapshot> destination)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
EnsureDirectionalShadowTransformJournal();
|
||||
ulong latest = _directionalShadowTransformRevision;
|
||||
if (afterRevision == latest)
|
||||
return new DirectionalShadowTransformChanges(latest, 0, false);
|
||||
if (afterRevision == 0
|
||||
|| afterRevision > latest
|
||||
|| latest - afterRevision
|
||||
> checked((ulong)_directionalShadowTransformChangeCount))
|
||||
{
|
||||
return new DirectionalShadowTransformChanges(latest, 0, true);
|
||||
}
|
||||
|
||||
int count = checked((int)(latest - afterRevision));
|
||||
if (destination.Length < count)
|
||||
return new DirectionalShadowTransformChanges(latest, 0, true);
|
||||
DirectionalShadowTransformChange[] journal =
|
||||
_directionalShadowTransformChanges!;
|
||||
int updateTransformCount = 0;
|
||||
int updateAppearanceCount = 0;
|
||||
int dynamicSynchronizationCount = 0;
|
||||
int activeAnimatedStaticCount = 0;
|
||||
int liveDynamicRootCount = 0;
|
||||
int equippedChildCount = 0;
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
ulong revision = checked(afterRevision + (ulong)index + 1UL);
|
||||
DirectionalShadowTransformChange change =
|
||||
journal[(int)(revision % (ulong)journal.Length)];
|
||||
if (change.Revision != revision)
|
||||
return new DirectionalShadowTransformChanges(latest, 0, true);
|
||||
destination[index] = change.Projection;
|
||||
switch (change.Kind)
|
||||
{
|
||||
case DirectionalShadowTransformChangeKind.UpdateTransform:
|
||||
updateTransformCount++;
|
||||
break;
|
||||
case DirectionalShadowTransformChangeKind.UpdateAppearance:
|
||||
updateAppearanceCount++;
|
||||
break;
|
||||
case DirectionalShadowTransformChangeKind.DynamicSynchronization:
|
||||
dynamicSynchronizationCount++;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown directional-shadow change kind {change.Kind}.");
|
||||
}
|
||||
switch (change.Projection.ProjectionClass)
|
||||
{
|
||||
case RenderProjectionClass.ActiveAnimatedStatic:
|
||||
activeAnimatedStaticCount++;
|
||||
break;
|
||||
case RenderProjectionClass.LiveDynamicRoot:
|
||||
liveDynamicRootCount++;
|
||||
break;
|
||||
case RenderProjectionClass.EquippedChild:
|
||||
equippedChildCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new DirectionalShadowTransformChanges(
|
||||
latest,
|
||||
count,
|
||||
false,
|
||||
updateTransformCount,
|
||||
updateAppearanceCount,
|
||||
dynamicSynchronizationCount,
|
||||
activeAnimatedStaticCount,
|
||||
liveDynamicRootCount,
|
||||
equippedChildCount);
|
||||
}
|
||||
|
||||
bool IRenderSceneQuerySource.TryGet(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionId id,
|
||||
|
|
@ -300,6 +434,30 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
return false;
|
||||
}
|
||||
|
||||
int IRenderSceneQuerySource.CopyById(
|
||||
RenderSceneGeneration generation,
|
||||
ReadOnlySpan<RenderProjectionId> ids,
|
||||
Span<RenderProjectionRecord> destination)
|
||||
{
|
||||
EnsureQueryGeneration(generation);
|
||||
if (destination.Length < ids.Length)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The render-scene ID-copy destination is too small.",
|
||||
nameof(destination));
|
||||
}
|
||||
for (int index = 0; index < ids.Length; index++)
|
||||
{
|
||||
if (!_entries.TryGetValue(ids[index], out SceneEntry entry))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Render-scene projection {ids[index]} disappeared during a batched copy.");
|
||||
}
|
||||
destination[index] = ReadRecord(in entry);
|
||||
}
|
||||
return ids.Length;
|
||||
}
|
||||
|
||||
int IRenderSceneQuerySource.CopyTo(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionClass? projectionClass,
|
||||
|
|
@ -391,6 +549,16 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
RenderProjectionRecord prior = ReadRecord(in existing);
|
||||
WriteRecord(existing.Entity, in record);
|
||||
UpdateIndices(in prior, in record);
|
||||
if (HasRefreshableDirectionalShadowTransforms(record.ProjectionClass))
|
||||
{
|
||||
if (!TransformBitsEqual(prior.Transform, record.Transform))
|
||||
{
|
||||
PublishDirectionalShadowTransformChange(
|
||||
in record,
|
||||
DirectionalShadowTransformChangeKind.UpdateTransform);
|
||||
}
|
||||
PublishDirectionalShadowPartPoseChangeIfNeeded(in record);
|
||||
}
|
||||
result.Applied++;
|
||||
result.Updated++;
|
||||
return;
|
||||
|
|
@ -407,6 +575,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
record.ProjectionClass);
|
||||
IncrementCount(record.ProjectionClass);
|
||||
AddToIndices(in record);
|
||||
SynchronizeDirectionalShadowPartPose(in record);
|
||||
result.Applied++;
|
||||
result.Registered++;
|
||||
}
|
||||
|
|
@ -465,6 +634,20 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
|
||||
RenderProjectionRecord current = ReadRecord(in entry);
|
||||
UpdateIndices(in prior, in current);
|
||||
if (HasRefreshableDirectionalShadowTransforms(current.ProjectionClass))
|
||||
{
|
||||
if (kind is RenderProjectionDeltaKind.UpdateTransform
|
||||
&& !TransformBitsEqual(prior.Transform, current.Transform))
|
||||
{
|
||||
PublishDirectionalShadowTransformChange(
|
||||
in current,
|
||||
DirectionalShadowTransformChangeKind.UpdateTransform);
|
||||
}
|
||||
else if (kind is RenderProjectionDeltaKind.UpdateAppearance)
|
||||
{
|
||||
PublishDirectionalShadowPartPoseChangeIfNeeded(in current);
|
||||
}
|
||||
}
|
||||
result.Applied++;
|
||||
result.Updated++;
|
||||
}
|
||||
|
|
@ -582,6 +765,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
private void Destroy(in SceneEntry entry)
|
||||
{
|
||||
RenderProjectionRecord record = ReadRecord(in entry);
|
||||
_directionalShadowPartPoses?.Remove(record.Id);
|
||||
RemoveFromIndices(in record);
|
||||
_world.Destroy(entry.Entity);
|
||||
DecrementCount(entry.ProjectionClass);
|
||||
|
|
@ -603,6 +787,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
{
|
||||
if (IndexMembershipEquals(in prior, in current))
|
||||
{
|
||||
if (!DirectionalShadowTopologyEquals(in prior, in current))
|
||||
AdvanceDirectionalShadowTopologyRevision();
|
||||
SynchronizeDirtyIndex(in current);
|
||||
return;
|
||||
}
|
||||
|
|
@ -652,6 +838,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
if (record.DirtyMask != RenderDirtyMask.None)
|
||||
_dirty.Add(record.Id);
|
||||
AdvanceIndexRevision();
|
||||
AdvanceDirectionalShadowTopologyRevision();
|
||||
}
|
||||
|
||||
private void RemoveFromIndices(in RenderProjectionRecord record)
|
||||
|
|
@ -668,6 +855,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
|
||||
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
|
||||
AdvanceIndexRevision();
|
||||
AdvanceDirectionalShadowTopologyRevision();
|
||||
}
|
||||
|
||||
private static bool IndexMembershipEquals(
|
||||
|
|
@ -689,6 +877,235 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
&& left.SortKey == right.SortKey;
|
||||
}
|
||||
|
||||
private static bool DirectionalShadowTopologyEquals(
|
||||
in RenderProjectionRecord left,
|
||||
in RenderProjectionRecord right)
|
||||
{
|
||||
const RenderProjectionFlags eligibilityFlags =
|
||||
RenderProjectionFlags.Draw
|
||||
| RenderProjectionFlags.SpatiallyResident
|
||||
| RenderProjectionFlags.Translucent;
|
||||
|
||||
if (left.ProjectionClass != right.ProjectionClass
|
||||
|| left.OwnerIncarnation != right.OwnerIncarnation
|
||||
|| left.Source.ParentCellId != right.Source.ParentCellId
|
||||
|| (left.Flags & eligibilityFlags) != (right.Flags & eligibilityFlags)
|
||||
|| left.SortKey != right.SortKey
|
||||
|| left.MeshSet.MeshCount != right.MeshSet.MeshCount
|
||||
|| left.Material != right.Material
|
||||
|| left.DegradeState != right.DegradeState
|
||||
|| left.Source.AppearanceFingerprint
|
||||
!= right.Source.AppearanceFingerprint
|
||||
|| left.Source.DirectionalShadowTopologyFingerprint
|
||||
!= right.Source.DirectionalShadowTopologyFingerprint
|
||||
|| left.EntityPayload.IsBuildingShell
|
||||
!= right.EntityPayload.IsBuildingShell
|
||||
|| left.EntityPayload.CasterIdentity
|
||||
!= right.EntityPayload.CasterIdentity
|
||||
|| !PaletteEquals(
|
||||
left.EntityPayload.PaletteOverride,
|
||||
right.EntityPayload.PaletteOverride))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool refreshableTransforms =
|
||||
HasRefreshableDirectionalShadowTransforms(left.ProjectionClass);
|
||||
if (!refreshableTransforms
|
||||
&& (left.Transform != right.Transform
|
||||
|| left.MeshSet != right.MeshSet
|
||||
|| left.Source.GeometryFingerprint
|
||||
!= right.Source.GeometryFingerprint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef>? leftMeshes =
|
||||
left.EntityPayload.MeshRefs;
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef>? rightMeshes =
|
||||
right.EntityPayload.MeshRefs;
|
||||
if (ReferenceEquals(leftMeshes, rightMeshes))
|
||||
return true;
|
||||
if (leftMeshes is null
|
||||
|| rightMeshes is null
|
||||
|| leftMeshes.Count != rightMeshes.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int meshIndex = 0; meshIndex < leftMeshes.Count; meshIndex++)
|
||||
{
|
||||
AcDream.Core.World.MeshRef leftMesh = leftMeshes[meshIndex];
|
||||
AcDream.Core.World.MeshRef rightMesh = rightMeshes[meshIndex];
|
||||
if (leftMesh.GfxObjId != rightMesh.GfxObjId
|
||||
|| !SurfaceOverridesEqual(
|
||||
leftMesh.SurfaceOverrides,
|
||||
rightMesh.SurfaceOverrides)
|
||||
|| (!refreshableTransforms
|
||||
&& leftMesh.PartTransform != rightMesh.PartTransform))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasRefreshableDirectionalShadowTransforms(
|
||||
RenderProjectionClass projectionClass) =>
|
||||
projectionClass is RenderProjectionClass.ActiveAnimatedStatic
|
||||
or RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
|
||||
private static bool TransformBitsEqual(
|
||||
in RenderTransform left,
|
||||
in RenderTransform right)
|
||||
{
|
||||
Matrix4x4 leftMatrix = left.LocalToWorld;
|
||||
Matrix4x4 rightMatrix = right.LocalToWorld;
|
||||
ReadOnlySpan<Matrix4x4> leftSpan = MemoryMarshal.CreateReadOnlySpan(
|
||||
in leftMatrix,
|
||||
1);
|
||||
ReadOnlySpan<Matrix4x4> rightSpan = MemoryMarshal.CreateReadOnlySpan(
|
||||
in rightMatrix,
|
||||
1);
|
||||
return MemoryMarshal.AsBytes(leftSpan).SequenceEqual(
|
||||
MemoryMarshal.AsBytes(rightSpan));
|
||||
}
|
||||
|
||||
private void EnsureDirectionalShadowTransformJournal()
|
||||
{
|
||||
if (_directionalShadowTransformChanges is not null)
|
||||
return;
|
||||
_directionalShadowTransformChanges = new DirectionalShadowTransformChange[
|
||||
DirectionalShadowTransformChangeJournal.Capacity];
|
||||
_directionalShadowTransformRevision = 1;
|
||||
_directionalShadowTransformChangeCount = 0;
|
||||
_directionalShadowPartPoses = new Dictionary<
|
||||
RenderProjectionId,
|
||||
DirectionalShadowPartPoseSnapshot>();
|
||||
foreach (SceneEntry entry in _entries.Values)
|
||||
{
|
||||
if (!HasRefreshableDirectionalShadowTransforms(entry.ProjectionClass))
|
||||
continue;
|
||||
RenderProjectionRecord record = ReadRecord(in entry);
|
||||
SynchronizeDirectionalShadowPartPose(in record);
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishDirectionalShadowTransformChange(
|
||||
in RenderProjectionRecord projection,
|
||||
DirectionalShadowTransformChangeKind kind)
|
||||
{
|
||||
DirectionalShadowTransformChange[]? journal =
|
||||
_directionalShadowTransformChanges;
|
||||
if (journal is null)
|
||||
return;
|
||||
if (_directionalShadowTransformRevision == ulong.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow transform revision space was exhausted.");
|
||||
}
|
||||
ulong revision = ++_directionalShadowTransformRevision;
|
||||
journal[(int)(revision % (ulong)journal.Length)] =
|
||||
new DirectionalShadowTransformChange(
|
||||
revision,
|
||||
DirectionalShadowTransformSnapshot.Capture(in projection),
|
||||
kind);
|
||||
if (_directionalShadowTransformChangeCount < journal.Length)
|
||||
_directionalShadowTransformChangeCount++;
|
||||
}
|
||||
|
||||
private void ResetDirectionalShadowTransformChanges()
|
||||
{
|
||||
if (_directionalShadowTransformChanges is null)
|
||||
return;
|
||||
_directionalShadowTransformRevision = 1;
|
||||
_directionalShadowTransformChangeCount = 0;
|
||||
_directionalShadowPartPoses!.Clear();
|
||||
}
|
||||
|
||||
private void SynchronizeDirectionalShadowPartPose(
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
|
||||
poses = _directionalShadowPartPoses;
|
||||
if (poses is null
|
||||
|| !HasRefreshableDirectionalShadowTransforms(record.ProjectionClass))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!poses.TryGetValue(record.Id, out DirectionalShadowPartPoseSnapshot? pose))
|
||||
{
|
||||
poses.Add(record.Id, DirectionalShadowPartPoseSnapshot.Capture(in record));
|
||||
return;
|
||||
}
|
||||
pose.CaptureCurrent(in record);
|
||||
}
|
||||
|
||||
private void PublishDirectionalShadowPartPoseChangeIfNeeded(
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
|
||||
poses = _directionalShadowPartPoses;
|
||||
if (poses is null)
|
||||
return;
|
||||
if (!poses.TryGetValue(record.Id, out DirectionalShadowPartPoseSnapshot? pose))
|
||||
{
|
||||
poses.Add(record.Id, DirectionalShadowPartPoseSnapshot.Capture(in record));
|
||||
return;
|
||||
}
|
||||
if (!pose.CaptureCurrent(in record))
|
||||
return;
|
||||
PublishDirectionalShadowTransformChange(
|
||||
in record,
|
||||
DirectionalShadowTransformChangeKind.UpdateAppearance);
|
||||
}
|
||||
|
||||
private static bool PaletteEquals(
|
||||
AcDream.Core.World.PaletteOverride? left,
|
||||
AcDream.Core.World.PaletteOverride? right)
|
||||
{
|
||||
if (ReferenceEquals(left, right))
|
||||
return true;
|
||||
if (left is null
|
||||
|| right is null
|
||||
|| left.BasePaletteId != right.BasePaletteId
|
||||
|| left.SubPalettes.Count != right.SubPalettes.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int index = 0; index < left.SubPalettes.Count; index++)
|
||||
{
|
||||
if (left.SubPalettes[index] != right.SubPalettes[index])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SurfaceOverridesEqual(
|
||||
IReadOnlyDictionary<uint, uint>? left,
|
||||
IReadOnlyDictionary<uint, uint>? right)
|
||||
{
|
||||
if (ReferenceEquals(left, right))
|
||||
return true;
|
||||
if (left is null || right is null || left.Count != right.Count)
|
||||
return false;
|
||||
|
||||
foreach ((uint surfaceId, uint textureId) in left)
|
||||
{
|
||||
if (!right.TryGetValue(surfaceId, out uint candidate)
|
||||
|| candidate != textureId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SynchronizeDirtyIndex(
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
|
|
@ -709,6 +1126,17 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_indexRevision++;
|
||||
}
|
||||
|
||||
private void AdvanceDirectionalShadowTopologyRevision()
|
||||
{
|
||||
if (_directionalShadowTopologyRevision == ulong.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow topology revision space was exhausted.");
|
||||
}
|
||||
|
||||
_directionalShadowTopologyRevision++;
|
||||
}
|
||||
|
||||
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
|
||||
projectionClass is RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
|
|
@ -925,10 +1353,71 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
hash.Add(record.Source.AppearanceFingerprint.Low);
|
||||
hash.Add(record.Source.AppearanceFingerprint.High);
|
||||
hash.Add(record.Source.CurrentProjectionFlags);
|
||||
hash.Add((byte)record.EntityPayload.CasterIdentity);
|
||||
}
|
||||
|
||||
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
||||
|
||||
private readonly record struct DirectionalShadowTransformChange(
|
||||
ulong Revision,
|
||||
DirectionalShadowTransformSnapshot Projection,
|
||||
DirectionalShadowTransformChangeKind Kind);
|
||||
|
||||
private sealed class DirectionalShadowPartPoseSnapshot
|
||||
{
|
||||
private Matrix4x4[] _parts;
|
||||
|
||||
private DirectionalShadowPartPoseSnapshot(Matrix4x4[] parts) =>
|
||||
_parts = parts;
|
||||
|
||||
internal int Count => _parts.Length;
|
||||
|
||||
internal static DirectionalShadowPartPoseSnapshot Capture(
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef>? meshes =
|
||||
record.EntityPayload.MeshRefs;
|
||||
var parts = new Matrix4x4[meshes?.Count ?? 0];
|
||||
for (int index = 0; index < parts.Length; index++)
|
||||
parts[index] = meshes![index].PartTransform;
|
||||
return new DirectionalShadowPartPoseSnapshot(parts);
|
||||
}
|
||||
|
||||
internal bool CaptureCurrent(in RenderProjectionRecord record)
|
||||
{
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef>? meshes =
|
||||
record.EntityPayload.MeshRefs;
|
||||
int count = meshes?.Count ?? 0;
|
||||
bool changed = _parts.Length != count;
|
||||
if (changed)
|
||||
_parts = new Matrix4x4[count];
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
Matrix4x4 current = meshes![index].PartTransform;
|
||||
if (!MatrixBitsEqual(in _parts[index], in current))
|
||||
{
|
||||
_parts[index] = current;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool MatrixBitsEqual(
|
||||
in Matrix4x4 left,
|
||||
in Matrix4x4 right)
|
||||
{
|
||||
ReadOnlySpan<Matrix4x4> leftSpan = MemoryMarshal.CreateReadOnlySpan(
|
||||
in left,
|
||||
1);
|
||||
ReadOnlySpan<Matrix4x4> rightSpan = MemoryMarshal.CreateReadOnlySpan(
|
||||
in right,
|
||||
1);
|
||||
return MemoryMarshal.AsBytes(leftSpan).SequenceEqual(
|
||||
MemoryMarshal.AsBytes(rightSpan));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct OutdoorStaticTag;
|
||||
|
||||
private readonly record struct IndoorCellStaticTag;
|
||||
|
|
|
|||
|
|
@ -834,6 +834,22 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
geometry.Add(fingerprint.High);
|
||||
}
|
||||
|
||||
internal static RenderSceneHash128
|
||||
CreateDirectionalShadowTopologyFingerprint(WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
StableRenderHash128 topology = StableRenderHash128.Create();
|
||||
topology.Add(entity.MeshRefs.Count);
|
||||
for (int meshIndex = 0; meshIndex < entity.MeshRefs.Count; meshIndex++)
|
||||
{
|
||||
MeshRef mesh = entity.MeshRefs[meshIndex];
|
||||
topology.Add(mesh.GfxObjId);
|
||||
AddSurfaceOverrides(ref topology, mesh.SurfaceOverrides);
|
||||
}
|
||||
|
||||
return topology.Finish();
|
||||
}
|
||||
|
||||
internal static RenderSceneHash128 CreateSurfaceOverrideFingerprint(
|
||||
IReadOnlyDictionary<uint, uint>? overrides)
|
||||
{
|
||||
|
|
|
|||
542
src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs
Normal file
542
src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// Projection-level membership. Opaque versus alpha-cutout remains an exact
|
||||
/// mesh-batch decision in the dispatcher; this product deliberately does not
|
||||
/// guess from an entity's texture set.
|
||||
/// </summary>
|
||||
internal enum DirectionalShadowCasterKind : byte
|
||||
{
|
||||
OutdoorStatic,
|
||||
Building,
|
||||
AnimatedStatic,
|
||||
LiveDynamic,
|
||||
EquippedChild,
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowCaster(
|
||||
RenderProjectionRecord Projection,
|
||||
DirectionalShadowCasterKind Kind)
|
||||
{
|
||||
public bool UsesCurrentAnimatedTransforms =>
|
||||
Projection.ProjectionClass
|
||||
is RenderProjectionClass.ActiveAnimatedStatic
|
||||
or RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
}
|
||||
|
||||
internal readonly struct DirectionalShadowChangedPose
|
||||
{
|
||||
internal DirectionalShadowChangedPose(
|
||||
int casterIndex,
|
||||
in DirectionalShadowTransformSnapshot snapshot)
|
||||
{
|
||||
CasterIndex = casterIndex;
|
||||
Snapshot = snapshot;
|
||||
}
|
||||
|
||||
internal readonly int CasterIndex;
|
||||
internal readonly DirectionalShadowTransformSnapshot Snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepted caster counts by the strongest class proven at render publication.
|
||||
/// TerrainCommands is populated by the terrain command producer. OutdoorStatics
|
||||
/// includes trees and all other outdoor DAT scenery; NonPlayerCreatures includes
|
||||
/// hostile monsters and non-hostile NPC creatures because neither source carries
|
||||
/// a narrower authoritative render-only discriminator.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowCasterClassDiagnostics(
|
||||
int TerrainCommands,
|
||||
int OutdoorStatics,
|
||||
int Buildings,
|
||||
int AnimatedStatics,
|
||||
int LocalPlayers,
|
||||
int RemotePlayers,
|
||||
int NonPlayerCreatures,
|
||||
int OtherLiveDynamics,
|
||||
int EquippedChildren);
|
||||
|
||||
internal readonly record struct DirectionalShadowCasterBuildStats(
|
||||
int SourceOutdoorStatics,
|
||||
int SourceOutdoorDynamics,
|
||||
int Accepted,
|
||||
int RejectedNotDrawable,
|
||||
int RejectedNotResident,
|
||||
int RejectedTransparent,
|
||||
int RejectedIndoor,
|
||||
int RejectedMissingMesh,
|
||||
int IndexCopies,
|
||||
int Classifications,
|
||||
int DynamicTransformRefreshes,
|
||||
bool TopologyRebuilt,
|
||||
int CopiedTransformChanges = 0,
|
||||
int DedupedChangedCasterSlots = 0,
|
||||
bool TransformJournalFullRefresh = false,
|
||||
int UpdateTransformChanges = 0,
|
||||
int UpdateAppearanceChanges = 0,
|
||||
int DynamicSynchronizationChanges = 0,
|
||||
int ActiveAnimatedStaticChanges = 0,
|
||||
int LiveDynamicRootChanges = 0,
|
||||
int EquippedChildChanges = 0,
|
||||
bool DensityBulkRefresh = false,
|
||||
int BatchedProjectionCopyCalls = 0)
|
||||
{
|
||||
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reusable, streaming-bounded caster product. It copies and classifies the
|
||||
/// render scene's two resident outdoor indices only when the scene's shadow
|
||||
/// topology revision changes. Stable frames retain those topology records and
|
||||
/// emit only deduplicated slim root/part pose changes for prepared matrix slots.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalShadowCasterFrame
|
||||
{
|
||||
private RenderProjectionRecord[] _outdoorStaticScratch = [];
|
||||
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
|
||||
private DirectionalShadowCaster[] _casters = [];
|
||||
private int[] _refreshCasterSlots = [];
|
||||
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
|
||||
private bool[] _changedCasterFlags = [];
|
||||
private RenderProjectionId[] _casterIds = [];
|
||||
private RenderProjectionClass[] _casterClasses = [];
|
||||
private RenderProjectionId[] _denseIdScratch = [];
|
||||
private RenderProjectionRecord[] _denseRecordScratch = [];
|
||||
private readonly DirectionalShadowTransformSnapshot[] _transformChangeScratch =
|
||||
new DirectionalShadowTransformSnapshot[
|
||||
DirectionalShadowTransformChangeJournal.Capacity];
|
||||
private readonly Dictionary<RenderProjectionId, int> _refreshCasterSlotById = [];
|
||||
private int _casterCount;
|
||||
private int _refreshCasterSlotCount;
|
||||
private int _changedCasterPoseCount;
|
||||
private ulong _topologyRevision;
|
||||
private ulong _transformRevision;
|
||||
private DirectionalShadowTransformChanges _lastTransformChanges;
|
||||
private bool _lastDensityBulkRefresh;
|
||||
private int _lastBatchedProjectionCopyCalls;
|
||||
|
||||
public RenderSceneGeneration Generation { get; private set; }
|
||||
|
||||
public ulong BuildSequence { get; private set; }
|
||||
|
||||
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
|
||||
_casters.AsSpan(0, _casterCount);
|
||||
|
||||
internal ReadOnlySpan<int> RefreshCasterSlots =>
|
||||
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
|
||||
|
||||
internal ReadOnlySpan<DirectionalShadowChangedPose> ChangedCasterPoses =>
|
||||
_changedCasterPoses.AsSpan(0, _changedCasterPoseCount);
|
||||
|
||||
internal ulong TransformRevision => _transformRevision;
|
||||
|
||||
public DirectionalShadowCasterBuildStats Stats { get; private set; }
|
||||
|
||||
public long RetainedScratchBytes =>
|
||||
checked(
|
||||
(long)_outdoorStaticScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_outdoorDynamicScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_casters.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
|
||||
+ (long)_refreshCasterSlots.Length * sizeof(int)
|
||||
+ (long)_changedCasterPoses.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
DirectionalShadowChangedPose>()
|
||||
+ _changedCasterFlags.Length
|
||||
+ (long)_casterIds.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
RenderProjectionId>()
|
||||
+ (long)_casterClasses.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
RenderProjectionClass>()
|
||||
+ (long)_denseIdScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionId>()
|
||||
+ (long)_denseRecordScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_transformChangeScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
DirectionalShadowTransformSnapshot>()
|
||||
+ (long)_refreshCasterSlotById.EnsureCapacity(0)
|
||||
* (sizeof(int)
|
||||
+ System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
KeyValuePair<RenderProjectionId, int>>()));
|
||||
|
||||
public void Build(in RenderSceneQuery query)
|
||||
{
|
||||
ulong topologyRevision = query.DirectionalShadowTopologyRevision;
|
||||
if (BuildSequence != 0
|
||||
&& Generation == query.Generation
|
||||
&& _topologyRevision == topologyRevision)
|
||||
{
|
||||
int refreshes = RefreshChangedTransforms(in query);
|
||||
Stats = Stats with
|
||||
{
|
||||
IndexCopies = 0,
|
||||
Classifications = 0,
|
||||
DynamicTransformRefreshes = refreshes,
|
||||
TopologyRebuilt = false,
|
||||
CopiedTransformChanges = _lastTransformChanges.Count,
|
||||
DedupedChangedCasterSlots = _changedCasterPoseCount,
|
||||
TransformJournalFullRefresh =
|
||||
_lastTransformChanges.RequiresFullRefresh,
|
||||
DensityBulkRefresh = _lastDensityBulkRefresh,
|
||||
BatchedProjectionCopyCalls = _lastBatchedProjectionCopyCalls,
|
||||
UpdateTransformChanges =
|
||||
_lastTransformChanges.UpdateTransformCount,
|
||||
UpdateAppearanceChanges =
|
||||
_lastTransformChanges.UpdateAppearanceCount,
|
||||
DynamicSynchronizationChanges =
|
||||
_lastTransformChanges.DynamicSynchronizationCount,
|
||||
ActiveAnimatedStaticChanges =
|
||||
_lastTransformChanges.ActiveAnimatedStaticCount,
|
||||
LiveDynamicRootChanges =
|
||||
_lastTransformChanges.LiveDynamicRootCount,
|
||||
EquippedChildChanges =
|
||||
_lastTransformChanges.EquippedChildCount,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
RenderSceneIndexCounts counts = query.IndexCounts;
|
||||
EnsureCapacity(ref _outdoorStaticScratch, counts.OutdoorStatic);
|
||||
EnsureCapacity(ref _outdoorDynamicScratch, counts.OutdoorDynamic);
|
||||
int staticCount = query.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorStatic,
|
||||
_outdoorStaticScratch.AsSpan(0, counts.OutdoorStatic));
|
||||
int dynamicCount = query.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorDynamic,
|
||||
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
|
||||
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
|
||||
_casterCount = 0;
|
||||
|
||||
int rejectedNotDrawable = 0;
|
||||
int rejectedNotResident = 0;
|
||||
int rejectedTransparent = 0;
|
||||
int rejectedIndoor = 0;
|
||||
int rejectedMissingMesh = 0;
|
||||
int outdoorStatics = 0;
|
||||
int buildings = 0;
|
||||
int animatedStatics = 0;
|
||||
int localPlayers = 0;
|
||||
int remotePlayers = 0;
|
||||
int nonPlayerCreatures = 0;
|
||||
int otherLiveDynamics = 0;
|
||||
int equippedChildren = 0;
|
||||
for (int i = 0; i < staticCount; i++)
|
||||
Add(_outdoorStaticScratch[i]);
|
||||
for (int i = 0; i < dynamicCount; i++)
|
||||
Add(_outdoorDynamicScratch[i]);
|
||||
|
||||
Array.Sort(
|
||||
_casters,
|
||||
0,
|
||||
_casterCount,
|
||||
DirectionalShadowCasterComparer.Instance);
|
||||
int refreshCasterCount = 0;
|
||||
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
||||
{
|
||||
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
||||
refreshCasterCount++;
|
||||
}
|
||||
EnsureCapacity(ref _refreshCasterSlots, refreshCasterCount);
|
||||
EnsureCapacity(ref _changedCasterPoses, refreshCasterCount);
|
||||
EnsureCapacity(ref _changedCasterFlags, _casterCount);
|
||||
EnsureCapacity(ref _casterIds, _casterCount);
|
||||
EnsureCapacity(ref _casterClasses, _casterCount);
|
||||
EnsureCapacity(ref _denseIdScratch, refreshCasterCount);
|
||||
EnsureCapacity(ref _denseRecordScratch, refreshCasterCount);
|
||||
_refreshCasterSlotCount = 0;
|
||||
_changedCasterPoseCount = 0;
|
||||
_refreshCasterSlotById.Clear();
|
||||
_refreshCasterSlotById.EnsureCapacity(refreshCasterCount);
|
||||
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
||||
{
|
||||
_casterIds[casterIndex] = _casters[casterIndex].Projection.Id;
|
||||
_casterClasses[casterIndex] =
|
||||
_casters[casterIndex].Projection.ProjectionClass;
|
||||
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
||||
{
|
||||
_refreshCasterSlots[_refreshCasterSlotCount++] = casterIndex;
|
||||
_refreshCasterSlotById.Add(
|
||||
_casterIds[casterIndex],
|
||||
casterIndex);
|
||||
}
|
||||
}
|
||||
Generation = query.Generation;
|
||||
_topologyRevision = topologyRevision;
|
||||
_transformRevision = query.DirectionalShadowTransformRevision;
|
||||
_lastTransformChanges = default;
|
||||
_lastDensityBulkRefresh = false;
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
BuildSequence = checked(BuildSequence + 1);
|
||||
Stats = new DirectionalShadowCasterBuildStats(
|
||||
staticCount,
|
||||
dynamicCount,
|
||||
_casterCount,
|
||||
rejectedNotDrawable,
|
||||
rejectedNotResident,
|
||||
rejectedTransparent,
|
||||
rejectedIndoor,
|
||||
rejectedMissingMesh,
|
||||
IndexCopies: 2,
|
||||
Classifications: _casterCount,
|
||||
DynamicTransformRefreshes: 0,
|
||||
TopologyRebuilt: true)
|
||||
{
|
||||
CasterClasses = new DirectionalShadowCasterClassDiagnostics(
|
||||
TerrainCommands: 0,
|
||||
outdoorStatics,
|
||||
buildings,
|
||||
animatedStatics,
|
||||
localPlayers,
|
||||
remotePlayers,
|
||||
nonPlayerCreatures,
|
||||
otherLiveDynamics,
|
||||
equippedChildren),
|
||||
};
|
||||
return;
|
||||
|
||||
void Add(in RenderProjectionRecord projection)
|
||||
{
|
||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||
{
|
||||
rejectedNotDrawable++;
|
||||
return;
|
||||
}
|
||||
if ((projection.Flags & RenderProjectionFlags.SpatiallyResident) == 0)
|
||||
{
|
||||
rejectedNotResident++;
|
||||
return;
|
||||
}
|
||||
// Transparent means a true blended projection. ClipMap/foliage is
|
||||
// retained here and separated from opaque batches later.
|
||||
if ((projection.Flags & RenderProjectionFlags.Translucent) != 0)
|
||||
{
|
||||
rejectedTransparent++;
|
||||
return;
|
||||
}
|
||||
if (projection.Source.ParentCellId != 0
|
||||
&& InteriorEntityPartition.IsIndoorCellId(
|
||||
projection.Source.ParentCellId))
|
||||
{
|
||||
rejectedIndoor++;
|
||||
return;
|
||||
}
|
||||
if (projection.MeshSet.MeshCount <= 0
|
||||
|| projection.EntityPayload.MeshRefs is null
|
||||
|| projection.EntityPayload.MeshRefs.Count == 0)
|
||||
{
|
||||
rejectedMissingMesh++;
|
||||
return;
|
||||
}
|
||||
|
||||
DirectionalShadowCasterKind kind = Classify(in projection);
|
||||
_casters[_casterCount++] = new DirectionalShadowCaster(
|
||||
projection,
|
||||
kind);
|
||||
switch (kind)
|
||||
{
|
||||
case DirectionalShadowCasterKind.OutdoorStatic:
|
||||
outdoorStatics++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.Building:
|
||||
buildings++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.AnimatedStatic:
|
||||
animatedStatics++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.EquippedChild:
|
||||
equippedChildren++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.LiveDynamic:
|
||||
switch (projection.EntityPayload.CasterIdentity)
|
||||
{
|
||||
case RenderCasterIdentityKind.LocalPlayer:
|
||||
localPlayers++;
|
||||
break;
|
||||
case RenderCasterIdentityKind.RemotePlayer:
|
||||
remotePlayers++;
|
||||
break;
|
||||
case RenderCasterIdentityKind.NonPlayerCreature:
|
||||
nonPlayerCreatures++;
|
||||
break;
|
||||
default:
|
||||
otherLiveDynamics++;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(kind), kind, "Unknown shadow caster kind.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int RefreshChangedTransforms(in RenderSceneQuery query)
|
||||
{
|
||||
_changedCasterPoseCount = 0;
|
||||
ulong latest = query.DirectionalShadowTransformRevision;
|
||||
if (latest == _transformRevision)
|
||||
{
|
||||
_lastTransformChanges = new DirectionalShadowTransformChanges(
|
||||
latest,
|
||||
0,
|
||||
false);
|
||||
_lastDensityBulkRefresh = false;
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DirectionalShadowTransformChanges changes =
|
||||
query.CopyDirectionalShadowTransformChanges(
|
||||
_transformRevision,
|
||||
_transformChangeScratch);
|
||||
_lastTransformChanges = changes;
|
||||
if (changes.RequiresFullRefresh)
|
||||
{
|
||||
_lastBatchedProjectionCopyCalls = 1;
|
||||
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
||||
{
|
||||
int casterIndex = _refreshCasterSlots[index];
|
||||
_denseIdScratch[index] = _casters[casterIndex].Projection.Id;
|
||||
}
|
||||
int copied = query.CopyById(
|
||||
_denseIdScratch.AsSpan(0, _refreshCasterSlotCount),
|
||||
_denseRecordScratch.AsSpan(0, _refreshCasterSlotCount));
|
||||
if (copied != _refreshCasterSlotCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dense directional-shadow refresh returned an incomplete record batch.");
|
||||
}
|
||||
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
||||
{
|
||||
int casterIndex = _refreshCasterSlots[index];
|
||||
RefreshOne(in _denseRecordScratch[index], casterIndex);
|
||||
DirectionalShadowTransformSnapshot snapshot =
|
||||
DirectionalShadowTransformSnapshot.Capture(
|
||||
in _denseRecordScratch[index]);
|
||||
_changedCasterPoses[_changedCasterPoseCount++] =
|
||||
new DirectionalShadowChangedPose(casterIndex, in snapshot);
|
||||
}
|
||||
_lastDensityBulkRefresh = false;
|
||||
_transformRevision = changes.LatestRevision;
|
||||
return _changedCasterPoseCount;
|
||||
}
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
|
||||
try
|
||||
{
|
||||
ReadOnlySpan<DirectionalShadowTransformSnapshot> records =
|
||||
_transformChangeScratch.AsSpan(0, changes.Count);
|
||||
// Newest-first makes repeated publications of the same projection
|
||||
// resolve to the latest exact root/part payload without an ECS read.
|
||||
for (int index = records.Length - 1; index >= 0; index--)
|
||||
{
|
||||
if (!_refreshCasterSlotById.TryGetValue(
|
||||
records[index].Id,
|
||||
out int casterIndex)
|
||||
|| _changedCasterFlags[casterIndex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_changedCasterFlags[casterIndex] = true;
|
||||
ValidateStablePose(in records[index], casterIndex);
|
||||
_changedCasterPoses[_changedCasterPoseCount++] =
|
||||
new DirectionalShadowChangedPose(
|
||||
casterIndex,
|
||||
in records[index]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int index = 0; index < _changedCasterPoseCount; index++)
|
||||
{
|
||||
_changedCasterFlags[
|
||||
_changedCasterPoses[index].CasterIndex] = false;
|
||||
}
|
||||
}
|
||||
_lastDensityBulkRefresh = _refreshCasterSlotCount >= 64
|
||||
&& _changedCasterPoseCount
|
||||
>= checked((_refreshCasterSlotCount * 3) / 4);
|
||||
_transformRevision = changes.LatestRevision;
|
||||
return _changedCasterPoseCount;
|
||||
}
|
||||
|
||||
private void ValidateStablePose(
|
||||
in DirectionalShadowTransformSnapshot current,
|
||||
int casterIndex)
|
||||
{
|
||||
if (current.Id != _casterIds[casterIndex]
|
||||
|| current.ProjectionClass != _casterClasses[casterIndex])
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stable directional-shadow topology changed caster "
|
||||
+ $"{_casterIds[casterIndex]} identity or class.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshOne(
|
||||
in RenderProjectionRecord current,
|
||||
int casterIndex)
|
||||
{
|
||||
DirectionalShadowCaster retained = _casters[casterIndex];
|
||||
if (current.Id != retained.Projection.Id
|
||||
|| current.ProjectionClass != retained.Projection.ProjectionClass)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stable directional-shadow topology changed caster "
|
||||
+ $"{retained.Projection.Id} identity or class.");
|
||||
}
|
||||
_casters[casterIndex] = retained with { Projection = current };
|
||||
}
|
||||
|
||||
private static DirectionalShadowCasterKind Classify(
|
||||
in RenderProjectionRecord projection)
|
||||
{
|
||||
if (projection.EntityPayload.IsBuildingShell)
|
||||
return DirectionalShadowCasterKind.Building;
|
||||
return projection.ProjectionClass switch
|
||||
{
|
||||
RenderProjectionClass.OutdoorStatic =>
|
||||
DirectionalShadowCasterKind.OutdoorStatic,
|
||||
RenderProjectionClass.ActiveAnimatedStatic =>
|
||||
DirectionalShadowCasterKind.AnimatedStatic,
|
||||
RenderProjectionClass.LiveDynamicRoot =>
|
||||
DirectionalShadowCasterKind.LiveDynamic,
|
||||
RenderProjectionClass.EquippedChild =>
|
||||
DirectionalShadowCasterKind.EquippedChild,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Outdoor shadow index carried unsupported {projection.ProjectionClass}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref T[] values, int required)
|
||||
{
|
||||
if (required < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(required));
|
||||
if (values.Length >= required)
|
||||
return;
|
||||
int capacity = values.Length == 0 ? 4 : values.Length;
|
||||
while (capacity < required)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref values, capacity);
|
||||
}
|
||||
|
||||
private sealed class DirectionalShadowCasterComparer
|
||||
: IComparer<DirectionalShadowCaster>
|
||||
{
|
||||
public static DirectionalShadowCasterComparer Instance { get; } = new();
|
||||
|
||||
public int Compare(DirectionalShadowCaster left, DirectionalShadowCaster right)
|
||||
{
|
||||
int order = left.Projection.SortKey.Value.CompareTo(
|
||||
right.Projection.SortKey.Value);
|
||||
return order != 0
|
||||
? order
|
||||
: left.Projection.Id.CompareTo(right.Projection.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly RenderProjectionJournal _journal;
|
||||
private readonly IRenderTraversalOrderSource _traversalOrder;
|
||||
private readonly ILocalPlayerIdentitySource? _localPlayer;
|
||||
private readonly Dictionary<RuntimeEntityKey, TrackedProjection> _byKey = [];
|
||||
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||
private readonly List<TrackedProjection> _activeScratch = [];
|
||||
|
|
@ -33,12 +36,14 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
public LiveRenderProjectionJournal(
|
||||
LiveEntityRuntime runtime,
|
||||
RenderProjectionJournal journal,
|
||||
IRenderTraversalOrderSource traversalOrder)
|
||||
IRenderTraversalOrderSource traversalOrder,
|
||||
ILocalPlayerIdentitySource? localPlayer = null)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
||||
_traversalOrder = traversalOrder
|
||||
?? throw new ArgumentNullException(nameof(traversalOrder));
|
||||
_localPlayer = localPlayer;
|
||||
}
|
||||
|
||||
public int ProjectionCount => _byKey.Count;
|
||||
|
|
@ -286,7 +291,14 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
ownerLandblockId,
|
||||
fullCellId,
|
||||
entity,
|
||||
spatiallyVisible);
|
||||
spatiallyVisible,
|
||||
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||
? RenderCasterIdentityKind.EquippedChild
|
||||
: RenderCasterIdentityClassifier.Classify(
|
||||
record.Snapshot.Guid,
|
||||
record.Snapshot.ItemType,
|
||||
record.Snapshot.ObjectDescriptionFlags,
|
||||
_localPlayer?.ServerGuid ?? 0u));
|
||||
if (_traversalOrder.TryGetTraversalSortKey(
|
||||
entity,
|
||||
out RenderSortKey sortKey))
|
||||
|
|
@ -336,6 +348,31 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
}
|
||||
}
|
||||
|
||||
internal static class RenderCasterIdentityClassifier
|
||||
{
|
||||
private const uint PlayerDescriptionFlag = 0x8u;
|
||||
private const uint PlayerGuidPrefix = 0x50000000u;
|
||||
|
||||
internal static RenderCasterIdentityKind Classify(
|
||||
uint serverGuid,
|
||||
uint? itemType,
|
||||
uint? objectDescriptionFlags,
|
||||
uint localPlayerGuid)
|
||||
{
|
||||
if (localPlayerGuid != 0 && serverGuid == localPlayerGuid)
|
||||
return RenderCasterIdentityKind.LocalPlayer;
|
||||
if ((objectDescriptionFlags.GetValueOrDefault()
|
||||
& PlayerDescriptionFlag) != 0
|
||||
|| (serverGuid & 0xFF000000u) == PlayerGuidPrefix)
|
||||
{
|
||||
return RenderCasterIdentityKind.RemotePlayer;
|
||||
}
|
||||
if ((itemType.GetValueOrDefault() & (uint)ItemType.Creature) != 0)
|
||||
return RenderCasterIdentityKind.NonPlayerCreature;
|
||||
return RenderCasterIdentityKind.OtherLiveDynamic;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveRenderProjectionResourceLifecycle(
|
||||
ILiveRenderProjectionSink sink) : ILiveEntityResourceLifecycle
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ internal static class RenderProjectionRecordFactory
|
|||
uint ownerLandblockId,
|
||||
uint fullCellId,
|
||||
WorldEntity entity,
|
||||
bool spatiallyVisible)
|
||||
bool spatiallyVisible,
|
||||
RenderCasterIdentityKind casterIdentity =
|
||||
RenderCasterIdentityKind.Unclassified)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
CurrentRenderProjectionFingerprint fingerprint =
|
||||
|
|
@ -80,11 +82,14 @@ internal static class RenderProjectionRecordFactory
|
|||
fingerprint.Transform,
|
||||
fingerprint.Geometry,
|
||||
fingerprint.Appearance,
|
||||
fingerprint.Flags),
|
||||
fingerprint.Flags,
|
||||
CurrentRenderSceneOracle
|
||||
.CreateDirectionalShadowTopologyFingerprint(entity)),
|
||||
new RenderEntityPayload(
|
||||
entity.MeshRefs,
|
||||
entity.PaletteOverride,
|
||||
entity.IsBuildingShell));
|
||||
entity.IsBuildingShell,
|
||||
casterIdentity));
|
||||
}
|
||||
|
||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||
|
|
|
|||
|
|
@ -222,7 +222,26 @@ internal readonly record struct RenderSourceMetadata(
|
|||
RenderSceneHash128 TransformFingerprint,
|
||||
RenderSceneHash128 GeometryFingerprint,
|
||||
RenderSceneHash128 AppearanceFingerprint,
|
||||
uint CurrentProjectionFlags = 0);
|
||||
uint CurrentProjectionFlags = 0,
|
||||
RenderSceneHash128 DirectionalShadowTopologyFingerprint = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render-only identity facts retained from the authoritative publication edge.
|
||||
/// Outdoor DAT scenery has no tree discriminator, and the create-object payload
|
||||
/// does not distinguish hostile monsters from other non-player creatures, so
|
||||
/// neither narrower category is guessed here.
|
||||
/// </summary>
|
||||
internal enum RenderCasterIdentityKind : byte
|
||||
{
|
||||
Unclassified,
|
||||
OutdoorStatic,
|
||||
Building,
|
||||
LocalPlayer,
|
||||
RemotePlayer,
|
||||
NonPlayerCreature,
|
||||
OtherLiveDynamic,
|
||||
EquippedChild,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed immutable presentation payload captured at a scene publication
|
||||
|
|
@ -234,7 +253,9 @@ internal readonly record struct RenderSourceMetadata(
|
|||
internal readonly record struct RenderEntityPayload(
|
||||
IReadOnlyList<MeshRef> MeshRefs,
|
||||
PaletteOverride? PaletteOverride,
|
||||
bool IsBuildingShell);
|
||||
bool IsBuildingShell,
|
||||
RenderCasterIdentityKind CasterIdentity =
|
||||
RenderCasterIdentityKind.Unclassified);
|
||||
|
||||
internal readonly record struct RenderProjectionRecord(
|
||||
RenderProjectionId Id,
|
||||
|
|
@ -449,6 +470,66 @@ internal readonly record struct RenderSceneDigest(
|
|||
RenderProjectionCounts Counts,
|
||||
RenderSceneHash128 Hash);
|
||||
|
||||
internal static class DirectionalShadowTransformChangeJournal
|
||||
{
|
||||
// Larger than the measured 9,498-caster dense-Arwic row so one complete
|
||||
// changed-pose publication fits without truncation. Overflow is explicit
|
||||
// and makes the consumer take its exact full-refresh fallback.
|
||||
internal const int Capacity = 16_384;
|
||||
}
|
||||
|
||||
internal readonly struct DirectionalShadowTransformSnapshot
|
||||
{
|
||||
internal DirectionalShadowTransformSnapshot(
|
||||
RenderProjectionId id,
|
||||
RenderProjectionClass projectionClass,
|
||||
RenderTransform transform,
|
||||
RenderEntityPayload entityPayload)
|
||||
{
|
||||
Id = id;
|
||||
ProjectionClass = projectionClass;
|
||||
Transform = transform;
|
||||
EntityPayload = entityPayload;
|
||||
}
|
||||
|
||||
internal readonly RenderProjectionId Id;
|
||||
internal readonly RenderProjectionClass ProjectionClass;
|
||||
internal readonly RenderTransform Transform;
|
||||
internal readonly RenderEntityPayload EntityPayload;
|
||||
|
||||
internal static DirectionalShadowTransformSnapshot Capture(
|
||||
in RenderProjectionRecord projection) =>
|
||||
new(
|
||||
projection.Id,
|
||||
projection.ProjectionClass,
|
||||
projection.Transform,
|
||||
projection.EntityPayload);
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowTransformChanges(
|
||||
ulong LatestRevision,
|
||||
int Count,
|
||||
bool RequiresFullRefresh,
|
||||
int UpdateTransformCount = 0,
|
||||
int UpdateAppearanceCount = 0,
|
||||
int DynamicSynchronizationCount = 0,
|
||||
int ActiveAnimatedStaticCount = 0,
|
||||
int LiveDynamicRootCount = 0,
|
||||
int EquippedChildCount = 0);
|
||||
|
||||
// Transform-journal records carry the producer's already-current projection.
|
||||
// Root matrices are values; MeshRef payloads are borrowed under the render
|
||||
// publication ordering rule: a part-pose mutation must be followed by its
|
||||
// UpdateAppearance publication before the frame opens a scene query. Consumers
|
||||
// read newest-to-oldest, so repeated IDs always select the latest publication.
|
||||
|
||||
internal enum DirectionalShadowTransformChangeKind : byte
|
||||
{
|
||||
UpdateTransform,
|
||||
UpdateAppearance,
|
||||
DynamicSynchronization,
|
||||
}
|
||||
|
||||
internal sealed class RenderSceneDigestBuffer
|
||||
{
|
||||
internal List<RenderProjectionRecord> Records { get; } = [];
|
||||
|
|
@ -461,12 +542,26 @@ internal interface IRenderSceneQuerySource
|
|||
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
||||
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
|
||||
ulong GetIndexRevision(RenderSceneGeneration generation);
|
||||
ulong GetDirectionalShadowTopologyRevision(
|
||||
RenderSceneGeneration generation);
|
||||
ulong GetDirectionalShadowTransformRevision(
|
||||
RenderSceneGeneration generation);
|
||||
|
||||
DirectionalShadowTransformChanges CopyDirectionalShadowTransformChanges(
|
||||
RenderSceneGeneration generation,
|
||||
ulong afterRevision,
|
||||
Span<DirectionalShadowTransformSnapshot> destination);
|
||||
|
||||
bool TryGet(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionId id,
|
||||
out RenderProjectionRecord record);
|
||||
|
||||
int CopyById(
|
||||
RenderSceneGeneration generation,
|
||||
ReadOnlySpan<RenderProjectionId> ids,
|
||||
Span<RenderProjectionRecord> destination);
|
||||
|
||||
int CopyTo(
|
||||
RenderSceneGeneration generation,
|
||||
RenderProjectionClass? projectionClass,
|
||||
|
|
@ -512,11 +607,30 @@ internal readonly struct RenderSceneQuery
|
|||
public ulong IndexRevision =>
|
||||
Source.GetIndexRevision(Generation);
|
||||
|
||||
public ulong DirectionalShadowTopologyRevision =>
|
||||
Source.GetDirectionalShadowTopologyRevision(Generation);
|
||||
|
||||
public ulong DirectionalShadowTransformRevision =>
|
||||
Source.GetDirectionalShadowTransformRevision(Generation);
|
||||
|
||||
public DirectionalShadowTransformChanges CopyDirectionalShadowTransformChanges(
|
||||
ulong afterRevision,
|
||||
Span<DirectionalShadowTransformSnapshot> destination) =>
|
||||
Source.CopyDirectionalShadowTransformChanges(
|
||||
Generation,
|
||||
afterRevision,
|
||||
destination);
|
||||
|
||||
public bool TryGet(
|
||||
RenderProjectionId id,
|
||||
out RenderProjectionRecord record) =>
|
||||
Source.TryGet(Generation, id, out record);
|
||||
|
||||
public int CopyById(
|
||||
ReadOnlySpan<RenderProjectionId> ids,
|
||||
Span<RenderProjectionRecord> destination) =>
|
||||
Source.CopyById(Generation, ids, destination);
|
||||
|
||||
public int CopyTo(Span<RenderProjectionRecord> destination) =>
|
||||
Source.CopyTo(Generation, null, destination);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -95,7 +96,8 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
|
|||
|
||||
public LiveRenderProjectionJournal BindLiveRuntime(
|
||||
LiveEntityRuntime runtime,
|
||||
IRenderTraversalOrderSource traversalOrder)
|
||||
IRenderTraversalOrderSource traversalOrder,
|
||||
ILocalPlayerIdentitySource? localPlayer = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
|
|
@ -109,7 +111,8 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
|
|||
_live = new LiveRenderProjectionJournal(
|
||||
runtime,
|
||||
_journal,
|
||||
traversalOrder);
|
||||
traversalOrder,
|
||||
localPlayer);
|
||||
return _live;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
&& accepted.Source.AppearanceFingerprint
|
||||
== retained.Record.Source.AppearanceFingerprint
|
||||
&& accepted.EntityPayload.IsBuildingShell
|
||||
== retained.Record.EntityPayload.IsBuildingShell)
|
||||
== retained.Record.EntityPayload.IsBuildingShell
|
||||
&& accepted.EntityPayload.CasterIdentity
|
||||
== retained.Record.EntityPayload.CasterIdentity)
|
||||
{
|
||||
accepted = accepted with
|
||||
{
|
||||
|
|
@ -245,7 +247,10 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
tracked.Record.Residency.OwnerLandblockId,
|
||||
tracked.Record.Residency.FullCellId,
|
||||
entity,
|
||||
spatiallyVisible: true) with
|
||||
spatiallyVisible: true,
|
||||
casterIdentity: entity.IsBuildingShell
|
||||
? RenderCasterIdentityKind.Building
|
||||
: RenderCasterIdentityKind.OutdoorStatic) with
|
||||
{
|
||||
PreviousTransform = new PreviousRenderTransform(
|
||||
tracked.Record.Transform.LocalToWorld),
|
||||
|
|
@ -317,7 +322,10 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
landblockId,
|
||||
fullCellId,
|
||||
entity,
|
||||
spatiallyVisible: true) with
|
||||
spatiallyVisible: true,
|
||||
casterIdentity: entity.IsBuildingShell
|
||||
? RenderCasterIdentityKind.Building
|
||||
: RenderCasterIdentityKind.OutdoorStatic) with
|
||||
{
|
||||
SortKey = sortKey,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Selection;
|
|||
/// </summary>
|
||||
internal interface IWorldSceneSelectionFrame
|
||||
{
|
||||
void BeginFrame();
|
||||
void BeginFrame(FrustumPlanes? preparedViewFrustum = null);
|
||||
|
||||
void CompleteFrame();
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ internal sealed class RetailSelectionScene :
|
|||
_lightingPulse.Clear();
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
public void BeginFrame(FrustumPlanes? preparedViewFrustum = null)
|
||||
{
|
||||
if (_frameOpen)
|
||||
{
|
||||
|
|
@ -87,7 +87,7 @@ internal sealed class RetailSelectionScene :
|
|||
_frameOpen = true;
|
||||
_building.Clear();
|
||||
_buildingKeys.Clear();
|
||||
_viewFrustum = null;
|
||||
_viewFrustum = preparedViewFrustum;
|
||||
_currentRenderSceneObserver?.BeginSelectionFrame();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 stepUv = uPackParams0.xy;
|
||||
vec3 value = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb * 0.227027;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv + stepUv * 1.384615).rgb * 0.316216;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv - stepUv * 1.384615).rgb * 0.316216;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv + stepUv * 3.230769).rgb * 0.070270;
|
||||
value += ACDREAM_SAMPLE_2D(uTextureIndexA, vUv - stepUv * 3.230769).rgb * 0.070270;
|
||||
oColor = vec4(value, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 scene = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexB, vUv).rgb;
|
||||
if (uPackParams0.w > 0.5)
|
||||
scene += ACDREAM_SAMPLE_2D(uTextureIndexC, vUv).rgb;
|
||||
float brightness = dot(scene, vec3(0.2126, 0.7152, 0.0722));
|
||||
float threshold = uPackParams0.y;
|
||||
float knee = max(uPackParams0.z, 0.0001);
|
||||
float soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0);
|
||||
soft = soft * soft;
|
||||
float contribution = max(brightness - threshold, 0.0) + soft * knee;
|
||||
contribution /= max(brightness, 0.0001);
|
||||
vec3 bloom = scene * contribution * uPackParams0.x;
|
||||
oColor = vec4(bloom, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
34
src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl
Normal file
34
src/AcDream.App/Rendering/Shaders/atmospheric_common.glsl
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef ACDREAM_ATMOSPHERIC_COMMON_GLSL
|
||||
#define ACDREAM_ATMOSPHERIC_COMMON_GLSL
|
||||
|
||||
// Render-pack shader ABI v1. These declarations are the byte-level SSOT for
|
||||
// AtmosphericFrameUniforms (set 3/binding 5, 160 bytes) and
|
||||
// AtmosphericPackPassUniforms (set 3/binding 7). Binding 6 is intentionally
|
||||
// reserved for directional-shadow data in directional_shadow_common.glsl.
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 5) uniform AtmosphericFrame {
|
||||
vec4 uAtmosphereSunScreen; // 0: uv.xy, ray strength, elevation degrees
|
||||
vec4 uAtmosphereSunColor; // 16: authored linear rgb, policy multiplier
|
||||
vec4 uAtmosphereViewport; // 32: width, height, reciprocal width/height
|
||||
vec4 uAtmosphereWeather; // 48: kind, intensity, delta seconds, outdoor
|
||||
vec4 uAtmosphereSunDirection; // 64: surface-to-sun xyz, authored brightness
|
||||
vec4 uAtmospherePolicy; // 80: day group, group factor, shadow/shaft elevation factors
|
||||
mat4 uAtmosphereInverseViewProjection; // 96: screen/depth to world
|
||||
};
|
||||
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 7) uniform PackPass {
|
||||
vec4 uPackParams0; // 0
|
||||
vec4 uPackParams1; // 16
|
||||
vec4 uPackParams2; // 32
|
||||
vec4 uPackParams3; // 48
|
||||
};
|
||||
|
||||
// FusedAtmosphericPostProcess PackPass ABI (opt-in Low preset only):
|
||||
// sun-rays: Params1 = (enabled, logical mask width, mask height, 0)
|
||||
// filmic: Params1.z = enabled; Params2 = bloom extraction parameters;
|
||||
// Params3.xy = logical bloom texel step
|
||||
|
||||
layout(std140, ACDREAM_PACK_UBO_SET binding = 8) uniform PackSettings {
|
||||
vec4 uPackSettings[16]; // 64 declaration-order scalar setting slots
|
||||
};
|
||||
|
||||
#endif
|
||||
88
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.frag
Normal file
88
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.frag
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
vec3 acesFitted(vec3 value)
|
||||
{
|
||||
const float a = 2.51;
|
||||
const float b = 0.03;
|
||||
const float c = 2.43;
|
||||
const float d = 0.59;
|
||||
const float e = 0.14;
|
||||
return clamp((value * (a * value + b)) / (value * (c * value + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 sampleBloom(vec2 uv)
|
||||
{
|
||||
return ACDREAM_SAMPLE_2D(uTextureIndexB, uv).rgb;
|
||||
}
|
||||
|
||||
vec3 lowFusedScene(vec2 uv)
|
||||
{
|
||||
vec3 scene = ACDREAM_SAMPLE_2D(uTextureIndexA, uv).rgb
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexB, uv).rgb;
|
||||
if (uPackParams2.w > 0.5)
|
||||
scene += ACDREAM_SAMPLE_2D(uTextureIndexC, uv).rgb;
|
||||
return scene;
|
||||
}
|
||||
|
||||
vec3 lowFusedBloomExtract(vec3 scene)
|
||||
{
|
||||
float brightness = dot(scene, vec3(0.2126, 0.7152, 0.0722));
|
||||
float threshold = uPackParams2.y;
|
||||
float knee = max(uPackParams2.z, 0.0001);
|
||||
float soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0);
|
||||
soft = soft * soft;
|
||||
float contribution = max(brightness - threshold, 0.0) + soft * knee;
|
||||
contribution /= max(brightness, 0.0001);
|
||||
return scene * contribution * uPackParams2.x;
|
||||
}
|
||||
|
||||
vec3 lowFusedBloom(vec3 centerScene)
|
||||
{
|
||||
const float offsets[5] = float[5](
|
||||
-3.230769, -1.384615, 0.0, 1.384615, 3.230769);
|
||||
const float weights[5] = float[5](
|
||||
0.070270, 0.316216, 0.227027, 0.316216, 0.070270);
|
||||
vec3 bloom = vec3(0.0);
|
||||
for (int y = 0; y < 5; ++y) {
|
||||
for (int x = 0; x < 5; ++x) {
|
||||
vec3 scene = x == 2 && y == 2
|
||||
? centerScene
|
||||
: lowFusedScene(vUv + vec2(offsets[x], offsets[y]) * uPackParams3.xy);
|
||||
bloom += lowFusedBloomExtract(scene) * (weights[x] * weights[y]);
|
||||
}
|
||||
}
|
||||
return bloom;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 hdr;
|
||||
if (uPackParams1.z > 0.5) {
|
||||
vec3 scene = lowFusedScene(vUv);
|
||||
hdr = scene + lowFusedBloom(scene);
|
||||
}
|
||||
else {
|
||||
hdr = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).rgb
|
||||
+ sampleBloom(vUv)
|
||||
+ ACDREAM_SAMPLE_2D(uTextureIndexC, vUv).rgb;
|
||||
if (uPackParams1.y > 0.5)
|
||||
hdr += ACDREAM_SAMPLE_2D(uTextureIndexD, vUv).rgb;
|
||||
}
|
||||
vec3 exposed = max(hdr * uPackParams0.x, vec3(0.0));
|
||||
vec3 linearClamped = clamp(exposed, 0.0, 1.0);
|
||||
vec3 color = mix(linearClamped, acesFitted(exposed), clamp(uPackParams1.x, 0.0, 1.0));
|
||||
|
||||
float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
||||
color = mix(vec3(luminance), color, uPackParams0.y);
|
||||
color = (color - 0.5) * uPackParams0.z + 0.5;
|
||||
|
||||
vec2 centered = vUv * 2.0 - 1.0;
|
||||
float vignette = smoothstep(1.25, 0.25, dot(centered, centered));
|
||||
color *= mix(1.0, vignette, clamp(uPackParams0.w, 0.0, 1.0));
|
||||
oColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
||||
}
|
||||
10
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.vert
Normal file
10
src/AcDream.App/Rendering/Shaders/atmospheric_filmic.vert
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) in vec2 vUv;
|
||||
layout(location = 0) out vec4 oColor;
|
||||
|
||||
#include "atmospheric_common.glsl"
|
||||
|
||||
void main()
|
||||
{
|
||||
float depth = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv).r;
|
||||
float unobstructedSky = smoothstep(0.9975, 0.99995, depth);
|
||||
float enabled = uAtmosphereSunScreen.z * uAtmosphereWeather.w;
|
||||
oColor = vec4(vec3(unobstructedSky * enabled), 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#version 430 core
|
||||
|
||||
layout(location = 0) out vec2 vUv;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 triangle = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
|
||||
gl_Position = vec4(triangle * 2.0 - 1.0, 0.0, 1.0);
|
||||
// Vulkan's negative viewport preserves GL world winding; flip the sampled
|
||||
// image coordinate once here so row zero remains the screen top.
|
||||
vUv = vec2(triangle.x, 1.0 - triangle.y);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue