Frame fractal updates as reservoir candidates

This commit is contained in:
Meta Vens 2026-05-19 15:01:56 +01:00
commit ddcac6dd49
7 changed files with 119 additions and 6 deletions

View file

@ -120,9 +120,10 @@ Built:
candidate count, and exposes the RIS contribution weight.
- `TemporalSpatialEvidenceReservoir` remains the stable track layer used by
temporal Gaussian fields and LocalCast fallback GPU fusion.
- `FractalContributionCache` remains the node-summary LOD estimator. It should
become a candidate generator feeding the resampling core rather than a
separate claim to temporal-reservoir theory.
- `FractalContributionCache` remains the node-summary LOD estimator, and now
frames scheduled update nodes as weighted reservoir candidates with a
per-frame reservoir snapshot for debug and tests. It is the candidate source,
not the full temporal/spatial reuse pass.
Not built yet:

View file

@ -333,7 +333,7 @@ Tasks:
- [x] Add exploration budget.
- [x] Add live contribution cache that observes only scheduled nodes each frame
and feeds the next resource plan.
- [ ] Convert contribution-cache refresh into a candidate generator that submits
- [x] Convert contribution-cache refresh into a candidate generator that submits
weighted candidates to the resampled-importance core.
- [ ] Add fractal SDF probe candidate records: domain key, local frame, bound,
projected contribution target, source PDF, payload handle, and material delta.
@ -354,6 +354,7 @@ Tests:
- [x] reservoir accepts candidates proportional to target/source PDF;
- [x] reservoir merge preserves represented candidate count and weight sum;
- [x] invalid zero-weight candidates do not corrupt reservoir state;
- [x] contribution cache exposes scheduled updates as reservoir candidates;
- deterministic RNG fixture repeats schedule;
- visible nodes keep nonzero update probability;
- near-threshold nodes keep nonzero update probability;

View file

@ -1,4 +1,5 @@
using Aquarium.Engine.Fractal;
using Aquarium.Engine.Fractal.Temporal;
namespace Aquarium.Engine.Fractal.Lod;
@ -7,6 +8,8 @@ public sealed class FractalContributionCache
private readonly FractalContributionTable table = new();
private uint frameIndex;
public FractalContributionReservoirSnapshot LastFrameReservoir { get; private set; }
public FractalResourcePlan PlanFrame(
IReadOnlyList<AquariumFractalSummary> summaries,
Func<AquariumFractalSummary, float> projectedPixelsPerWorldUnit,
@ -39,6 +42,7 @@ public sealed class FractalContributionCache
payloadStore,
frameIndex);
var reservoir = new ResampledImportanceReservoir<FractalContributionCandidate>();
var resident = plan.Residency.ResidentNodes.Select(key => key.Value).ToHashSet(StringComparer.Ordinal);
foreach (var nodeKey in plan.UpdateNodes)
{
@ -48,9 +52,19 @@ public sealed class FractalContributionCache
}
var state = table.GetOrCreate(nodeKey);
table.Store(FractalContributionEstimator.Observe(state, observedScore, frameIndex, resident.Contains(nodeKey.Value)));
var updateProbability = FractalStochasticUpdateScheduler.UpdateProbability(state, observedScore, frameIndex);
var isResident = resident.Contains(nodeKey.Value);
var candidate = FractalContributionCandidateGenerator.Build(
nodeKey,
observedScore,
updateProbability,
frameIndex,
isResident);
reservoir.Add(candidate, random.NextDouble());
table.Store(FractalContributionEstimator.Observe(state, observedScore, frameIndex, isResident));
}
LastFrameReservoir = FractalContributionCandidateGenerator.Snapshot(reservoir);
return plan;
}

View file

@ -0,0 +1,71 @@
using Aquarium.Engine.Fractal.Temporal;
namespace Aquarium.Engine.Fractal.Lod;
public readonly record struct FractalContributionCandidate(
AquariumFractalKey NodeKey,
float ObservedContribution,
float UpdateProbability,
uint FrameIndex,
bool Resident);
public readonly record struct FractalContributionReservoirSnapshot(
bool HasSample,
AquariumFractalKey SelectedNodeKey,
float SelectedContribution,
float SelectedTarget,
float WeightSum,
int CandidateCount,
float ContributionWeight);
public static class FractalContributionCandidateGenerator
{
public static ResampledImportanceCandidate<FractalContributionCandidate> Build(
AquariumFractalKey nodeKey,
float observedContribution,
double updateProbability,
uint frameIndex,
bool resident)
{
var target = MathF.Max(observedContribution, 0.0f);
var sourcePdf = MathF.Max((float)updateProbability, 1.0e-6f);
var sample = new FractalContributionCandidate(
nodeKey,
target,
sourcePdf,
frameIndex,
resident);
return new ResampledImportanceCandidate<FractalContributionCandidate>(
sample,
target,
sourcePdf);
}
public static FractalContributionReservoirSnapshot Snapshot(
ResampledImportanceReservoir<FractalContributionCandidate> reservoir)
{
ArgumentNullException.ThrowIfNull(reservoir);
if (!reservoir.HasSample)
{
return new FractalContributionReservoirSnapshot(
HasSample: false,
SelectedNodeKey: default,
SelectedContribution: 0.0f,
SelectedTarget: 0.0f,
WeightSum: reservoir.WeightSum,
CandidateCount: reservoir.CandidateCount,
ContributionWeight: 0.0f);
}
var selected = reservoir.SelectedSample;
return new FractalContributionReservoirSnapshot(
HasSample: true,
SelectedNodeKey: selected.NodeKey,
SelectedContribution: selected.ObservedContribution,
SelectedTarget: reservoir.SelectedTarget,
WeightSum: reservoir.WeightSum,
CandidateCount: reservoir.CandidateCount,
ContributionWeight: reservoir.ContributionWeight);
}
}

View file

@ -159,3 +159,4 @@
{"timestamp":"2026-05-19T16:11:00+01:00","topic":"zyphos-budgeted-fractal-geometry","lesson":"Authored fractal packets now affect Zyphos traced planet geometry, but only through a hard-budgeted top-influence subset. Selected height brushes are sorted by abs(amplitude)*max(radius), `sdfDistance` samples the first four, and `sdfSurface` still evaluates the full selected packet table for material/detail. A 12-brush geometry loop was tested and cut after pushing the live frame to about 33 ms; the four-brush budget returns the live view to about 16.4 ms after shader pipelines are ready.","files":["src/Aquarium.Zyphos/ZyphosFractalTerrain.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","tests/Aquarium.Engine.Fractal.Tests/ZyphosFractalTerrainTests.cs"],"source":"runtime proof after moving authored fractal packet evaluation into the hot SDF distance path"}
{"timestamp":"2026-05-19T16:27:00+01:00","topic":"zyphos-fractal-domain-debug-mode","lesson":"Render debug mode 11 now exposes a client fractal-domain view. Zyphos colors cube faces, draws an 8x8 tile grid, and overlays authored brush material influence so face/tile/material packet mapping can be inspected from the renderer instead of inferred from final beauty shading.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","research/rendering/fractal-brush-implementation-roadmap.md"],"source":"roadmap called for face/tile/LOD/cache debug modes; this is the first renderer-visible face/tile/material influence slice"}
{"timestamp":"2026-05-19T17:35:00+01:00","topic":"restir-reservoir-core","lesson":"The previous stable-key temporal smoother was not ReSTIR. Aquarium now treats `ResampledImportanceReservoir<TSample>` as the RIS/ReSTIR math authority: selected sample, target value, weight sum, represented candidate count, merge, and contribution weight. `TemporalSpatialEvidenceReservoir` is a stable resolved-track layer, while fractal contribution scoring should become a candidate generator feeding reservoir resampling with temporal/spatial/domain validation and TAA guide-buffer integration.","files":["src/Aquarium.Engine.Fractal/Temporal/ResampledImportanceCandidate.cs","src/Aquarium.Engine.Fractal/Temporal/ResampledImportanceReservoir.cs","tests/Aquarium.Engine.Fractal.Tests/ResampledImportanceReservoirTests.cs","docs/temporal-spatial-evidence-reservoir.md","research/rendering/fractal-brush-implementation-roadmap.md","state/map.yaml","state/memory.json"],"source":"user corrected the architecture toward NVIDIA ReSTIR/GRIS rather than invented spatiotemporal reservoir sampling"}
{"timestamp":"2026-05-19T18:03:00+01:00","topic":"fractal-contribution-candidates","lesson":"FractalContributionCache now frames scheduled update nodes as weighted `ResampledImportanceReservoir<FractalContributionCandidate>` candidates and exposes a per-frame reservoir snapshot for debug/tests. This is the handoff from heuristic LOD scoring into reservoir evidence; temporal/spatial reuse and domain shift validation still need to be built explicitly before calling the fractal path ReSTIR-complete.","files":["src/Aquarium.Engine.Fractal/Lod/FractalContributionCandidate.cs","src/Aquarium.Engine.Fractal/Lod/FractalContributionCache.cs","tests/Aquarium.Engine.Fractal.Tests/FractalContributionCacheTests.cs","docs/temporal-spatial-evidence-reservoir.md","research/rendering/fractal-brush-implementation-roadmap.md","state/memory.json"],"source":"continued Perfect Machine work after adding the ReSTIR-style reservoir core"}

View file

@ -89,7 +89,7 @@
"Zyphos passes cube-tile domain metadata through height brush packets and binds those packets into the SDF pass. The planet shader may use authored brush packets for post-hit material/detail evaluation; traced geometry must wait for selected-cut/resident packet policy so SDF distance loops do not evaluate every brush at every step.",
"Zyphos traced planet geometry may sample only a hard-budgeted top-influence subset of authored fractal brush packets in `sdfDistance`; the current proven budget is four brushes after sorting selected packets by abs(amplitude)*max(radius). A 12-brush distance loop was tested and rejected because it pushed the live frame to about 33 ms; the four-brush loop returns the live view to about 16.4 ms while full packet richness remains available to post-hit material/detail.",
"Temporal resolve has a cache-facing detail lane: SdfSurface.temporalDetail writes through scene-control.z into history-control.z, and D3D12Post reduces history confidence when current and previous temporal detail disagree. Use this lane for stochastic fractal/cache stability signals, not as geometry authority.",
"FractalContributionCache is the live cache loop: each frame builds a resource plan from current summaries and cached states, stochastically observes only budgeted update nodes, stores mean/variance/confidence/sample age through FractalContributionEstimator, and lets backend packet compilation stay cached by selected-cut key. It should evolve into a candidate generator feeding ResampledImportanceReservoir<TSample>, not pretend the estimator alone is spatiotemporal reservoir sampling. Do not replace this with a static plan cache that stops learning after the first camera bucket.",
"FractalContributionCache is the live cache loop: each frame builds a resource plan from current summaries and cached states, stochastically observes only budgeted update nodes, stores mean/variance/confidence/sample age through FractalContributionEstimator, frames scheduled updates as weighted ResampledImportanceReservoir candidates, and lets backend packet compilation stay cached by selected-cut key. It is a candidate source, not the whole temporal/spatial reuse pass. Do not replace this with a static plan cache that stops learning after the first camera bucket.",
"Zyphos camera distance limits belong to the selected spatial domain, not one global planetary clamp. Leaf/pebble domains must be able to zoom below whole-body distances, and the parent star must be a visible SDF body when it is also the primary light source.",
"The consolidated fractal brush architecture lives in research/rendering/fractal-brush-architecture-plan.md. Its pipeline is client intent -> domain roots -> semantic IFS brush grammar -> ownership tree -> node summaries -> contribution cache/residency scheduler -> backend packets -> renderer passes -> debug/evidence. Its credibility depends on module boundaries and tests: contracts, pure CPU fractal core, D3D12 backend, client adapters, mock payload/probe/random/clock seams, shader parity tests, and deterministic performance fixtures.",
"The actionable fractal brush roadmap lives in research/rendering/fractal-brush-implementation-roadmap.md. Start with the first work packet: add Aquarium.Engine.Fractal and tests, cube-face/tile key types, identity and tangent projection candidates, an area distortion sampler, face-edge tests, and a projection report before touching recursive detail."

View file

@ -28,6 +28,10 @@ public sealed class FractalContributionCacheTests
Assert.True(cache.TryGet(plan.UpdateNodes[0], out var updated));
Assert.Equal(1, updated.SampleCount);
Assert.True(updated.Confidence > 0.0f);
Assert.True(cache.LastFrameReservoir.HasSample);
Assert.Equal(plan.UpdateNodes[0], cache.LastFrameReservoir.SelectedNodeKey);
Assert.Equal(1, cache.LastFrameReservoir.CandidateCount);
Assert.True(cache.LastFrameReservoir.WeightSum > 0.0f);
}
[Fact]
@ -46,6 +50,27 @@ public sealed class FractalContributionCacheTests
Assert.True(state.Resident);
}
[Fact]
public void CacheFramesScheduledContributionAsReservoirCandidate()
{
var cache = new FractalContributionCache();
var summary = Summary("node/bright", maxHeightError: 3.0f);
cache.PlanFrame(
[summary],
_ => 4.0f,
new FractalResourceBudget(MaxCpuUpdates: 1, MaxGpuEstimatedCost: 4.0f, MaxResidentPayloads: 4, MaxSsdRequests: 0),
new TestFractalRandom(0.01, 0.0),
new ResidentStore());
var reservoir = cache.LastFrameReservoir;
Assert.True(reservoir.HasSample);
Assert.Equal(summary.NodeKey, reservoir.SelectedNodeKey);
Assert.True(reservoir.SelectedContribution > 0.0f);
Assert.True(reservoir.SelectedTarget > 0.0f);
Assert.True(reservoir.ContributionWeight > 0.0f);
}
private static AquariumFractalSummary Summary(string key, float maxHeightError)
{
return new AquariumFractalSummary(