Frame fractal updates as reservoir candidates
This commit is contained in:
parent
32cb3a30c3
commit
ddcac6dd49
7 changed files with 119 additions and 6 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue